diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.test.tsx index 200abf294e47b..bd0cfac44f69d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { TestProviders } from '../../../../common/mock'; import type { RulePreviewProps } from '.'; @@ -131,4 +132,51 @@ describe('PreviewQuery', () => { expect(await wrapper.queryByTestId('[data-test-subj="preview-histogram-panel"]')).toBeNull(); }); + + test('it renders quick/advanced query toggle button', async () => { + const wrapper = render( + + + + ); + + expect(await wrapper.findByTestId('quickAdvancedToggleButtonGroup')).toBeTruthy(); + }); + + test('it renders timeframe, interval and look-back buttons when advanced query is selected', async () => { + const wrapper = render( + + + + ); + + expect(await wrapper.findByTestId('quickAdvancedToggleButtonGroup')).toBeTruthy(); + const advancedQueryButton = await wrapper.findByTestId('advancedQuery'); + userEvent.click(advancedQueryButton); + expect(await wrapper.findByTestId('detectionEnginePreviewRuleInterval')).toBeTruthy(); + expect(await wrapper.findByTestId('detectionEnginePreviewRuleLookback')).toBeTruthy(); + }); + + test('it renders invocation count warning when advanced query is selected and warning flag is set to true', async () => { + (usePreviewRoute as jest.Mock).mockReturnValue({ + hasNoiseWarning: false, + addNoiseWarning: jest.fn(), + createPreview: jest.fn(), + clearPreview: jest.fn(), + logs: [], + isPreviewRequestInProgress: false, + previewId: undefined, + showInvocationCountWarning: true, + }); + + const wrapper = render( + + + + ); + + const advancedQueryButton = await wrapper.findByTestId('advancedQuery'); + userEvent.click(advancedQueryButton); + expect(await wrapper.findByTestId('previewInvocationCountWarning')).toBeTruthy(); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.tsx index b7bc9d5593cf3..d38627a03bf7d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.tsx @@ -6,17 +6,23 @@ */ import React, { useState, useEffect, useMemo, useCallback } from 'react'; +import dateMath from '@kbn/datemath'; import type { Unit } from '@kbn/datemath'; import type { ThreatMapping, Type } from '@kbn/securitysolution-io-ts-alerting-types'; import styled from 'styled-components'; +import type { EuiButtonGroupOptionProps, OnTimeChangeProps } from '@elastic/eui'; import { + EuiButtonGroup, + EuiFormErrorText, EuiFlexGroup, EuiFlexItem, EuiSelect, EuiFormRow, EuiButton, EuiSpacer, + EuiSuperDatePicker, } from '@elastic/eui'; +import moment from 'moment'; import { useSecurityJobs } from '../../../../common/components/ml_popover/hooks/use_security_jobs'; import type { FieldValueQueryBar } from '../query_bar'; import * as i18n from './translations'; @@ -31,6 +37,10 @@ import { isJobStarted } from '../../../../../common/machine_learning/helpers'; import type { EqlOptionsSelected } from '../../../../../common/search_strategy'; import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction'; import { SINGLE_RULE_ACTIONS } from '../../../../common/lib/apm/user_actions'; +import { Form, UseField, useForm, useFormData } from '../../../../shared_imports'; +import { ScheduleItem } from '../schedule_item_form'; +import type { AdvancedPreviewForm } from '../../../pages/detection_engine/rules/types'; +import { schema } from './schema'; const HelpTextComponent = ( @@ -39,6 +49,25 @@ const HelpTextComponent = ( ); +const timeRanges = [ + { start: 'now/d', end: 'now', label: 'Today' }, + { start: 'now/w', end: 'now', label: 'This week' }, + { start: 'now-15m', end: 'now', label: 'Last 15 minutes' }, + { start: 'now-30m', end: 'now', label: 'Last 30 minutes' }, + { start: 'now-1h', end: 'now', label: 'Last 1 hour' }, + { start: 'now-24h', end: 'now', label: 'Last 24 hours' }, + { start: 'now-7d', end: 'now', label: 'Last 7 days' }, + { start: 'now-30d', end: 'now', label: 'Last 30 days' }, +]; + +const QUICK_QUERY_SELECT_ID = 'quickQuery'; +const ADVANCED_QUERY_SELECT_ID = 'advancedQuery'; + +const advancedOptionsDefaultValue = { + interval: '5m', + lookback: '1m', +}; + export interface RulePreviewProps { index: string[]; isDisabled: boolean; @@ -92,6 +121,20 @@ const RulePreviewComponent: React.FC = ({ } }, [spaces]); + const [startDate, setStartDate] = useState('now-1h'); + const [endDate, setEndDate] = useState('now'); + + const { form } = useForm({ + defaultValue: advancedOptionsDefaultValue, + options: { stripEmptyFields: false }, + schema, + }); + + const [{ interval: formInterval, lookback: formLookback }] = useFormData({ + form, + watch: ['interval', 'lookback'], + }); + const areRelaventMlJobsRunning = useMemo(() => { if (ruleType !== 'machine_learning') { return true; // Don't do the expensive logic if we don't need it @@ -102,6 +145,43 @@ const RulePreviewComponent: React.FC = ({ } }, [jobs, machineLearningJobId, ruleType, isMlLoading]); + const [queryPreviewIdSelected, setQueryPreviewRadioIdSelected] = useState(QUICK_QUERY_SELECT_ID); + + // Callback for when user toggles between Quick query and Advanced query preview + const onChangeDataSource = (optionId: string) => { + setQueryPreviewRadioIdSelected(optionId); + }; + + const quickAdvancedToggleButtonOptions: EuiButtonGroupOptionProps[] = useMemo( + () => [ + { + id: QUICK_QUERY_SELECT_ID, + label: i18n.QUICK_PREVIEW_TOGGLE_BUTTON, + 'data-test-subj': `rule-preview-toggle-${QUICK_QUERY_SELECT_ID}`, + }, + { + id: ADVANCED_QUERY_SELECT_ID, + label: i18n.ADVANCED_PREVIEW_TOGGLE_BUTTON, + 'data-test-subj': `rule-index-toggle-${ADVANCED_QUERY_SELECT_ID}`, + }, + ], + [] + ); + + const showAdvancedOptions = queryPreviewIdSelected === ADVANCED_QUERY_SELECT_ID; + const advancedOptions = useMemo( + () => + showAdvancedOptions && startDate && endDate && formInterval && formLookback + ? { + timeframeStart: dateMath.parse(startDate) || moment().subtract(1, 'hour'), + timeframeEnd: dateMath.parse(endDate) || moment(), + interval: formInterval, + lookback: formLookback, + } + : undefined, + [endDate, formInterval, formLookback, showAdvancedOptions, startDate] + ); + const [timeFrame, setTimeFrame] = useState(defaultTimeRange); const { addNoiseWarning, @@ -111,6 +191,7 @@ const RulePreviewComponent: React.FC = ({ logs, hasNoiseWarning, isAborted, + showInvocationCountWarning, } = usePreviewRoute({ index, isDisabled, @@ -127,6 +208,7 @@ const RulePreviewComponent: React.FC = ({ eqlOptions, newTermsFields, historyWindowSize, + advancedOptions, }); // Resets the timeFrame to default when rule type is changed because not all time frames are supported by all rule types @@ -141,8 +223,36 @@ const RulePreviewComponent: React.FC = ({ createPreview(); }, [createPreview, startTransaction]); + const onTimeChange = useCallback( + ({ start: newStart, end: newEnd, isInvalid }: OnTimeChangeProps) => { + if (!isInvalid) { + setStartDate(newStart); + setEndDate(newEnd); + } + }, + [] + ); + return ( <> + + + + {showAdvancedOptions && showInvocationCountWarning && ( + <> + + {i18n.QUERY_PREVIEW_INVOCATION_COUNT_WARNING} + + + + )} = ({ > - setTimeFrame(e.target.value as Unit)} + aria-label={i18n.QUERY_PREVIEW_SELECT_ARIA} + disabled={isDisabled} + data-test-subj="preview-time-frame" + /> + )} = ({ - + {showAdvancedOptions && ( +
+ + + + + + )} {isPreviewRequestInProgress && } {!isPreviewRequestInProgress && previewId && spaceId && ( = ({ addNoiseWarning={addNoiseWarning} spaceId={spaceId} index={index} + advancedOptions={advancedOptions} /> )} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.test.tsx index 59c0af79391bd..7f4e79c0a41fa 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { render } from '@testing-library/react'; +import moment from 'moment'; import { useGlobalTime } from '../../../../common/containers/use_global_time'; import { TestProviders } from '../../../../common/mock'; @@ -96,4 +97,62 @@ describe('PreviewHistogram', () => { expect(await wrapper.findByTestId('preview-histogram-loading')).toBeTruthy(); }); }); + + describe('when advanced options passed', () => { + test('it uses timeframeStart and timeframeEnd to specify the time range of the preview', async () => { + const format = 'YYYY-MM-DD HH:mm:ss'; + const start = '2015-03-12 05:17:10'; + const end = '2020-03-12 05:17:10'; + + const usePreviewHistogramMock = usePreviewHistogram as jest.Mock; + usePreviewHistogramMock.mockReturnValue([ + true, + { + inspect: { dsl: [], response: [] }, + totalCount: 1, + refetch: jest.fn(), + data: [], + buckets: [], + }, + ]); + + usePreviewHistogramMock.mockImplementation( + ({ startDate, endDate }: { startDate: string; endDate: string }) => { + expect(startDate).toEqual('2015-03-12T09:17:10.000Z'); + expect(endDate).toEqual('2020-03-12T09:17:10.000Z'); + return [ + true, + { + inspect: { dsl: [], response: [] }, + totalCount: 1, + refetch: jest.fn(), + data: [], + buckets: [], + }, + ]; + } + ); + + const wrapper = render( + + + + ); + + expect(await wrapper.findByTestId('preview-histogram-loading')).toBeTruthy(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx index 36e765119e5e7..3c38e6e1a1e17 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx @@ -41,6 +41,7 @@ import { useGlobalFullScreen } from '../../../../common/containers/use_full_scre import { InspectButtonContainer } from '../../../../common/components/inspect'; import { timelineActions } from '../../../../timelines/store/timeline'; import type { State } from '../../../../common/store'; +import type { AdvancedPreviewOptions } from '../../../pages/detection_engine/rules/types'; const LoadingChart = styled(EuiLoadingChart)` display: block; @@ -63,6 +64,7 @@ interface PreviewHistogramProps { spaceId: string; ruleType: Type; index: string[]; + advancedOptions?: AdvancedPreviewOptions; } const DEFAULT_HISTOGRAM_HEIGHT = 300; @@ -74,14 +76,22 @@ export const PreviewHistogram = ({ spaceId, ruleType, index, + advancedOptions, }: PreviewHistogramProps) => { const dispatch = useDispatch(); const { setQuery, isInitializing } = useGlobalTime(); const { timelines: timelinesUi } = useKibana().services; const from = useMemo(() => `now-1${timeFrame}`, [timeFrame]); const to = useMemo(() => 'now', []); - const startDate = useMemo(() => formatDate(from), [from]); - const endDate = useMemo(() => formatDate(to), [to]); + const startDate = useMemo( + () => (advancedOptions ? advancedOptions.timeframeStart.toISOString() : formatDate(from)), + [from, advancedOptions] + ); + const endDate = useMemo( + () => (advancedOptions ? advancedOptions.timeframeEnd.toISOString() : formatDate(to)), + [to, advancedOptions] + ); + const alertsEndDate = useMemo(() => formatDate(to), [to]); const isEqlRule = useMemo(() => ruleType === 'eql', [ruleType]); const isMlRule = useMemo(() => ruleType === 'machine_learning', [ruleType]); @@ -204,7 +214,7 @@ export const PreviewHistogram = ({ dataProviders, deletedEventIds, disabledCellActions: FIELDS_WITHOUT_CELL_ACTIONS, - end: endDate, + end: alertsEndDate, entityType: 'events', filters: [], globalFullScreen, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/schema.tsx new file mode 100644 index 0000000000000..4f70a20b0fd36 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/schema.tsx @@ -0,0 +1,43 @@ +/* + * 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. + */ + +/* istanbul ignore file */ + +import { i18n } from '@kbn/i18n'; + +import { OptionalFieldLabel } from '../optional_field_label'; +import type { AdvancedPreviewForm } from '../../../pages/detection_engine/rules/types'; +import type { FormSchema } from '../../../../shared_imports'; + +export const schema: FormSchema = { + interval: { + label: i18n.translate('xpack.securitySolution.detectionEngine.previewRule.fieldIntervalLabel', { + defaultMessage: 'Runs every', + }), + helpText: i18n.translate( + 'xpack.securitySolution.detectionEngine.previewRule.fieldIntervalHelpText', + { + defaultMessage: 'Rules run periodically and detect alerts within the specified time frame.', + } + ), + }, + lookback: { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.previewRule.fieldAdditionalLookBackLabel', + { + defaultMessage: 'Additional look-back time', + } + ), + labelAppend: OptionalFieldLabel, + helpText: i18n.translate( + 'xpack.securitySolution.detectionEngine.previewRule.fieldAdditionalLookBackHelpText', + { + defaultMessage: 'Adds time to the look-back period to prevent missed alerts.', + } + ), + }, +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/translations.ts index 4d8679b91f21b..d6b3ae54eda34 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/translations.ts @@ -30,6 +30,20 @@ export const QUERY_PREVIEW_BUTTON = i18n.translate( } ); +export const QUICK_PREVIEW_TOGGLE_BUTTON = i18n.translate( + 'xpack.securitySolution.stepDefineRule.quickPreviewToggleButton', + { + defaultMessage: 'Quick query preview', + } +); + +export const ADVANCED_PREVIEW_TOGGLE_BUTTON = i18n.translate( + 'xpack.securitySolution.stepDefineRule.advancedPreviewToggleButton', + { + defaultMessage: 'Advanced query preview', + } +); + export const PREVIEW_TIMEOUT_WARNING = i18n.translate( 'xpack.securitySolution.stepDefineRule.previewTimeoutWarning', { @@ -47,7 +61,7 @@ export const QUERY_PREVIEW_SELECT_ARIA = i18n.translate( export const QUERY_PREVIEW_LABEL = i18n.translate( 'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewLabel', { - defaultMessage: 'Quick query preview', + defaultMessage: 'Timeframe', } ); @@ -58,6 +72,14 @@ export const QUERY_PREVIEW_HELP_TEXT = i18n.translate( } ); +export const QUERY_PREVIEW_INVOCATION_COUNT_WARNING = i18n.translate( + 'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewInvocationCountWarning', + { + defaultMessage: + 'The timeframe and rule interval that you selected might cause timeout or take long time to execute.', + } +); + export const QUERY_GRAPH_COUNT = i18n.translate( 'xpack.securitySolution.detectionEngine.queryPreview.queryGraphCountLabel', { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/use_preview_route.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/use_preview_route.tsx index 5b0251669e445..94817129fc9d0 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/use_preview_route.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/use_preview_route.tsx @@ -14,6 +14,7 @@ import { formatPreviewRule } from '../../../pages/detection_engine/rules/create/ import type { FieldValueThreshold } from '../threshold_input'; import type { RulePreviewLogs } from '../../../../../common/detection_engine/schemas/request'; import type { EqlOptionsSelected } from '../../../../../common/search_strategy'; +import type { AdvancedPreviewOptions } from '../../../pages/detection_engine/rules/types'; interface PreviewRouteParams { isDisabled: boolean; @@ -31,6 +32,7 @@ interface PreviewRouteParams { eqlOptions: EqlOptionsSelected; newTermsFields: string[]; historyWindowSize: string; + advancedOptions?: AdvancedPreviewOptions; } export const usePreviewRoute = ({ @@ -49,10 +51,14 @@ export const usePreviewRoute = ({ eqlOptions, newTermsFields, historyWindowSize, + advancedOptions, }: PreviewRouteParams) => { const [isRequestTriggered, setIsRequestTriggered] = useState(false); - const { isLoading, response, rule, setRule } = usePreviewRule(timeFrame); + const { isLoading, showInvocationCountWarning, response, rule, setRule } = usePreviewRule({ + timeframe: timeFrame, + advancedOptions, + }); const [logs, setLogs] = useState(response.logs ?? []); const [isAborted, setIsAborted] = useState(!!response.isAborted); const [hasNoiseWarning, setHasNoiseWarning] = useState(false); @@ -92,6 +98,7 @@ export const usePreviewRoute = ({ eqlOptions, newTermsFields, historyWindowSize, + advancedOptions, ]); useEffect(() => { @@ -112,6 +119,7 @@ export const usePreviewRoute = ({ eqlOptions, newTermsFields, historyWindowSize, + advancedOptions, }) ); } @@ -133,6 +141,7 @@ export const usePreviewRoute = ({ eqlOptions, newTermsFields, historyWindowSize, + advancedOptions, ]); return { @@ -144,5 +153,6 @@ export const usePreviewRoute = ({ previewId: response.previewId ?? '', logs, isAborted, + showInvocationCountWarning, }; }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_preview_rule.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_preview_rule.ts index a31cee9309d6a..423ca9d5ebc55 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_preview_rule.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_preview_rule.ts @@ -6,6 +6,7 @@ */ import { useEffect, useState } from 'react'; +import moment from 'moment'; import type { Unit } from '@kbn/datemath'; import { @@ -22,6 +23,10 @@ import type { import { previewRule } from './api'; import * as i18n from './translations'; import { transformOutput } from './transforms'; +import type { AdvancedPreviewOptions } from '../../../pages/detection_engine/rules/types'; +import { getTimeTypeValue } from '../../../pages/detection_engine/rules/create/helpers'; + +const REASONABLE_INVOCATION_COUNT = 200; const emptyPreviewRule: PreviewResponse = { previewId: undefined, @@ -29,14 +34,20 @@ const emptyPreviewRule: PreviewResponse = { isAborted: false, }; -export const usePreviewRule = (timeframe: Unit = 'h') => { +export const usePreviewRule = ({ + timeframe = 'h', + advancedOptions, +}: { + timeframe: Unit; + advancedOptions?: AdvancedPreviewOptions; +}) => { const [rule, setRule] = useState(null); const [response, setResponse] = useState(emptyPreviewRule); const [isLoading, setIsLoading] = useState(false); const { addError } = useAppToasts(); let invocationCount = RULE_PREVIEW_INVOCATION_COUNT.HOUR; - let interval = RULE_PREVIEW_INTERVAL.HOUR; - let from = RULE_PREVIEW_FROM.HOUR; + let interval: string = RULE_PREVIEW_INTERVAL.HOUR; + let from: string = RULE_PREVIEW_FROM.HOUR; switch (timeframe) { case 'd': @@ -56,6 +67,24 @@ export const usePreviewRule = (timeframe: Unit = 'h') => { break; } + if (advancedOptions) { + const timeframeDuration = + (advancedOptions.timeframeEnd.valueOf() / 1000 - + advancedOptions.timeframeStart.valueOf() / 1000) * + 1000; + + const { unit: intervalUnit, value: intervalValue } = getTimeTypeValue(advancedOptions.interval); + const { unit: lookbackUnit, value: lookbackValue } = getTimeTypeValue(advancedOptions.lookback); + const duration = moment.duration(intervalValue, intervalUnit as 's' | 'm' | 'h'); + duration.add(lookbackValue, lookbackUnit as 's' | 'm' | 'h'); + const ruleIntervalDuration = duration.asMilliseconds(); + + invocationCount = Math.max(Math.ceil(timeframeDuration / ruleIntervalDuration), 1); + interval = advancedOptions.interval; + from = `now-${duration.asSeconds()}s`; + } + const showInvocationCountWarning = invocationCount > REASONABLE_INVOCATION_COUNT; + useEffect(() => { if (!rule) { setResponse(emptyPreviewRule); @@ -103,5 +132,5 @@ export const usePreviewRule = (timeframe: Unit = 'h') => { }; }, [rule, addError, invocationCount, from, interval]); - return { isLoading, response, rule, setRule }; + return { isLoading, showInvocationCountWarning, response, rule, setRule }; }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts index a3da3f49587d3..c50d602c086bd 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts @@ -38,6 +38,7 @@ import type { ActionsStepRuleJson, RuleStepsFormData, RuleStep, + AdvancedPreviewOptions, } from '../types'; import type { FieldValueQueryBar } from '../../../../components/rules/query_bar'; import type { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request'; @@ -564,6 +565,7 @@ export const formatPreviewRule = ({ eqlOptions, newTermsFields, historyWindowSize, + advancedOptions, }: { index: string[]; dataViewId?: string; @@ -579,6 +581,7 @@ export const formatPreviewRule = ({ eqlOptions: EqlOptionsSelected; newTermsFields: string[]; historyWindowSize: string; + advancedOptions?: AdvancedPreviewOptions; }): CreateRulesSchema => { const defineStepData = { ...stepDefineDefaultValue, @@ -601,10 +604,16 @@ export const formatPreviewRule = ({ name: 'Preview Rule', description: 'Preview Rule', }; - const scheduleStepData = { + let scheduleStepData = { from: `now-${timeFrame === 'M' ? '25h' : timeFrame === 'd' ? '65m' : '6m'}`, interval: `${timeFrame === 'M' ? '1d' : timeFrame === 'd' ? '1h' : '5m'}`, }; + if (advancedOptions) { + scheduleStepData = { + interval: advancedOptions.interval, + from: advancedOptions.lookback, + }; + } return { ...formatRule( defineStepData, @@ -612,6 +621,6 @@ export const formatPreviewRule = ({ scheduleStepData, stepActionsDefaultValue ), - ...scheduleStepData, + ...(!advancedOptions ? scheduleStepData : {}), }; }; 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 cd4a41d1d3f19..2ff8b84f469f4 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 @@ -235,3 +235,15 @@ export interface ActionsStepRuleJson { throttle?: string | null; meta?: unknown; } + +export interface AdvancedPreviewForm { + interval: string; + lookback: string; +} + +export interface AdvancedPreviewOptions { + timeframeStart: moment.Moment; + timeframeEnd: moment.Moment; + interval: string; + lookback: string; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts index e528db104a486..1589afed7bac5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts @@ -51,7 +51,6 @@ import { createNewTermsAlertType, } from '../../rule_types'; import { createSecurityRuleTypeWrapper } from '../../rule_types/create_security_rule_type_wrapper'; -import { RULE_PREVIEW_INVOCATION_COUNT } from '../../../../../common/detection_engine/constants'; import type { RuleExecutionContext, StatusChangeArgs } from '../../rule_execution_log'; import { assertUnreachable } from '../../../../../common/utility_types'; import { wrapSearchSourceClient } from './utils/wrap_search_source_client'; @@ -92,14 +91,7 @@ export const previewRulesRoute = async ( const siemClient = (await context.securitySolution).getAppClient(); let invocationCount = request.body.invocationCount; - if ( - ![ - RULE_PREVIEW_INVOCATION_COUNT.HOUR, - RULE_PREVIEW_INVOCATION_COUNT.DAY, - RULE_PREVIEW_INVOCATION_COUNT.WEEK, - RULE_PREVIEW_INVOCATION_COUNT.MONTH, - ].includes(invocationCount) - ) { + if (invocationCount < 1) { return response.ok({ body: { logs: [{ errors: ['Invalid invocation count'], warnings: [], duration: 0 }] }, });