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}
+
+
+ >
+ )}
= ({
>
-
= ({
-
+ {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 }] },
});