mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Security Solution][Detections] Preview Rule: Make it possible to configure the time interval and look-back time (#137102)
* [Security Solution][Detections] Preview Rule: Make it possible to configure the time interval and look-back time (#4362) * Fix CI * Design updates * Design updates * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * Fix CI Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
e0280ea2f1
commit
8c005cf15e
16 changed files with 438 additions and 38 deletions
|
@ -415,7 +415,7 @@ export type CreateRulesSchema = t.TypeOf<typeof createRulesSchema>;
|
|||
export const previewRulesSchema = t.intersection([
|
||||
sharedCreateSchema,
|
||||
createTypeSpecific,
|
||||
t.type({ invocationCount: t.number }),
|
||||
t.type({ invocationCount: t.number, timeframeEnd: t.string }),
|
||||
]);
|
||||
export type PreviewRulesSchema = t.TypeOf<typeof previewRulesSchema>;
|
||||
|
||||
|
|
|
@ -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(
|
||||
<TestProviders>
|
||||
<RulePreview {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(await wrapper.findByTestId('quickAdvancedToggleButtonGroup')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it renders timeframe, interval and look-back buttons when advanced query is selected', async () => {
|
||||
const wrapper = render(
|
||||
<TestProviders>
|
||||
<RulePreview {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
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(
|
||||
<TestProviders>
|
||||
<RulePreview {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
const advancedQueryButton = await wrapper.findByTestId('advancedQuery');
|
||||
userEvent.click(advancedQueryButton);
|
||||
expect(await wrapper.findByTestId('previewInvocationCountWarning')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
EuiCallOut,
|
||||
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 = (
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
|
@ -39,6 +49,25 @@ const HelpTextComponent = (
|
|||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
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<RulePreviewProps> = ({
|
|||
}
|
||||
}, [spaces]);
|
||||
|
||||
const [startDate, setStartDate] = useState('now-1h');
|
||||
const [endDate, setEndDate] = useState('now');
|
||||
|
||||
const { form } = useForm<AdvancedPreviewForm>({
|
||||
defaultValue: advancedOptionsDefaultValue,
|
||||
options: { stripEmptyFields: false },
|
||||
schema,
|
||||
});
|
||||
|
||||
const [{ interval: formInterval, lookback: formLookback }] = useFormData<AdvancedPreviewForm>({
|
||||
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<RulePreviewProps> = ({
|
|||
}
|
||||
}, [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<Unit>(defaultTimeRange);
|
||||
const {
|
||||
addNoiseWarning,
|
||||
|
@ -111,6 +191,7 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
|
|||
logs,
|
||||
hasNoiseWarning,
|
||||
isAborted,
|
||||
showInvocationCountWarning,
|
||||
} = usePreviewRoute({
|
||||
index,
|
||||
isDisabled,
|
||||
|
@ -127,6 +208,7 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
|
|||
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,40 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
|
|||
createPreview();
|
||||
}, [createPreview, startTransaction]);
|
||||
|
||||
const onTimeChange = useCallback(
|
||||
({ start: newStart, end: newEnd, isInvalid }: OnTimeChangeProps) => {
|
||||
if (!isInvalid) {
|
||||
setStartDate(newStart);
|
||||
setEndDate(newEnd);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<EuiButtonGroup
|
||||
legend="Quick query or advanced query preview selector"
|
||||
data-test-subj="quickAdvancedToggleButtonGroup"
|
||||
idSelected={queryPreviewIdSelected}
|
||||
onChange={onChangeDataSource}
|
||||
options={quickAdvancedToggleButtonOptions}
|
||||
color="primary"
|
||||
/>
|
||||
<EuiSpacer />
|
||||
{showAdvancedOptions && showInvocationCountWarning && (
|
||||
<>
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
title={i18n.QUERY_PREVIEW_INVOCATION_COUNT_WARNING_TITLE}
|
||||
data-test-subj="previewInvocationCountWarning"
|
||||
>
|
||||
{i18n.QUERY_PREVIEW_INVOCATION_COUNT_WARNING_MESSAGE}
|
||||
</EuiCallOut>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
)}
|
||||
<EuiFormRow
|
||||
label={i18n.QUERY_PREVIEW_LABEL}
|
||||
helpText={HelpTextComponent}
|
||||
|
@ -153,15 +267,26 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
|
|||
>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={1}>
|
||||
<Select
|
||||
id="preview-time-frame"
|
||||
options={getTimeframeOptions(ruleType)}
|
||||
value={timeFrame}
|
||||
onChange={(e) => setTimeFrame(e.target.value as Unit)}
|
||||
aria-label={i18n.QUERY_PREVIEW_SELECT_ARIA}
|
||||
disabled={isDisabled}
|
||||
data-test-subj="preview-time-frame"
|
||||
/>
|
||||
{showAdvancedOptions ? (
|
||||
<EuiSuperDatePicker
|
||||
start={startDate}
|
||||
end={endDate}
|
||||
onTimeChange={onTimeChange}
|
||||
showUpdateButton={false}
|
||||
isDisabled={isDisabled}
|
||||
commonlyUsedRanges={timeRanges}
|
||||
/>
|
||||
) : (
|
||||
<Select
|
||||
id="preview-time-frame"
|
||||
options={getTimeframeOptions(ruleType)}
|
||||
value={timeFrame}
|
||||
onChange={(e) => setTimeFrame(e.target.value as Unit)}
|
||||
aria-label={i18n.QUERY_PREVIEW_SELECT_ARIA}
|
||||
disabled={isDisabled}
|
||||
data-test-subj="preview-time-frame"
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<PreviewButton
|
||||
|
@ -176,7 +301,31 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="s" />
|
||||
{showAdvancedOptions && (
|
||||
<Form form={form} data-test-subj="previewRule">
|
||||
<EuiSpacer size="s" />
|
||||
<UseField
|
||||
path="interval"
|
||||
component={ScheduleItem}
|
||||
componentProps={{
|
||||
idAria: 'detectionEnginePreviewRuleInterval',
|
||||
isDisabled,
|
||||
dataTestSubj: 'detectionEnginePreviewRuleInterval',
|
||||
}}
|
||||
/>
|
||||
<UseField
|
||||
path="lookback"
|
||||
component={ScheduleItem}
|
||||
componentProps={{
|
||||
idAria: 'detectionEnginePreviewRuleLookback',
|
||||
isDisabled,
|
||||
dataTestSubj: 'detectionEnginePreviewRuleLookback',
|
||||
minimumValue: 1,
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
</Form>
|
||||
)}
|
||||
{isPreviewRequestInProgress && <LoadingHistogram />}
|
||||
{!isPreviewRequestInProgress && previewId && spaceId && (
|
||||
<PreviewHistogram
|
||||
|
@ -186,6 +335,7 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
|
|||
addNoiseWarning={addNoiseWarning}
|
||||
spaceId={spaceId}
|
||||
index={index}
|
||||
advancedOptions={advancedOptions}
|
||||
/>
|
||||
)}
|
||||
<PreviewLogsComponent logs={logs} hasNoiseWarning={hasNoiseWarning} isAborted={isAborted} />
|
||||
|
|
|
@ -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(
|
||||
<TestProviders>
|
||||
<PreviewHistogram
|
||||
addNoiseWarning={jest.fn()}
|
||||
timeFrame="M"
|
||||
previewId={'test-preview-id'}
|
||||
spaceId={'default'}
|
||||
ruleType={'query'}
|
||||
index={['']}
|
||||
advancedOptions={{
|
||||
timeframeStart: moment(start, format),
|
||||
timeframeEnd: moment(end, format),
|
||||
interval: '5m',
|
||||
lookback: '1m',
|
||||
}}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(await wrapper.findByTestId('preview-histogram-loading')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<AdvancedPreviewForm> = {
|
||||
interval: {
|
||||
label: i18n.translate('xpack.securitySolution.detectionEngine.previewRule.fieldIntervalLabel', {
|
||||
defaultMessage: 'Runs every (Rule interval)',
|
||||
}),
|
||||
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.',
|
||||
}
|
||||
),
|
||||
},
|
||||
};
|
|
@ -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,20 @@ export const QUERY_PREVIEW_HELP_TEXT = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const QUERY_PREVIEW_INVOCATION_COUNT_WARNING_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewInvocationCountWarningTitle',
|
||||
{
|
||||
defaultMessage: 'Rule preview timeframe might cause timeout',
|
||||
}
|
||||
);
|
||||
|
||||
export const QUERY_PREVIEW_INVOCATION_COUNT_WARNING_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewInvocationCountWarningMessage',
|
||||
{
|
||||
defaultMessage: `The timeframe and rule interval that you selected for previewing this rule might cause timeout or take long time to execute. Try to decrease the timeframe and/or increase the interval if preview has timed out (this won't affect the actual rule run).`,
|
||||
}
|
||||
);
|
||||
|
||||
export const QUERY_GRAPH_COUNT = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.queryPreview.queryGraphCountLabel',
|
||||
{
|
||||
|
|
|
@ -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<RulePreviewLogs[]>(response.logs ?? []);
|
||||
const [isAborted, setIsAborted] = useState<boolean>(!!response.isAborted);
|
||||
const [hasNoiseWarning, setHasNoiseWarning] = useState<boolean>(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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -96,9 +96,12 @@ describe('Detections Rules API', () => {
|
|||
|
||||
test('POSTs rule', async () => {
|
||||
const payload = getCreateRulesSchemaMock();
|
||||
await previewRule({ rule: { ...payload, invocationCount: 1 }, signal: abortCtrl.signal });
|
||||
await previewRule({
|
||||
rule: { ...payload, invocationCount: 1, timeframeEnd: '2015-03-12 05:17:10' },
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/preview', {
|
||||
body: '{"description":"Detecting root and admin users","name":"Query with a rule id","query":"user.name: root or user.name: admin","severity":"high","type":"query","risk_score":55,"language":"kuery","rule_id":"rule-1","invocationCount":1}',
|
||||
body: '{"description":"Detecting root and admin users","name":"Query with a rule id","query":"user.name: root or user.name: admin","severity":"high","type":"query","risk_score":55,"language":"kuery","rule_id":"rule-1","invocationCount":1,"timeframeEnd":"2015-03-12 05:17:10"}',
|
||||
method: 'POST',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
|
|
@ -77,7 +77,7 @@ export interface CreateRulesProps {
|
|||
}
|
||||
|
||||
export interface PreviewRulesProps {
|
||||
rule: CreateRulesSchema & { invocationCount: number };
|
||||
rule: CreateRulesSchema & { invocationCount: number; timeframeEnd: string };
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, 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<CreateRulesSchema | null>(null);
|
||||
const [response, setResponse] = useState<PreviewResponse>(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':
|
||||
|
@ -55,6 +66,28 @@ export const usePreviewRule = (timeframe: Unit = 'h') => {
|
|||
from = RULE_PREVIEW_FROM.MONTH;
|
||||
break;
|
||||
}
|
||||
const timeframeEnd = useMemo(
|
||||
() => (advancedOptions ? advancedOptions.timeframeEnd.toISOString() : moment().toISOString()),
|
||||
[advancedOptions]
|
||||
);
|
||||
|
||||
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) {
|
||||
|
@ -79,6 +112,7 @@ export const usePreviewRule = (timeframe: Unit = 'h') => {
|
|||
from,
|
||||
}),
|
||||
invocationCount,
|
||||
timeframeEnd,
|
||||
},
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
@ -101,7 +135,7 @@ export const usePreviewRule = (timeframe: Unit = 'h') => {
|
|||
isSubscribed = false;
|
||||
abortCtrl.abort();
|
||||
};
|
||||
}, [rule, addError, invocationCount, from, interval]);
|
||||
}, [rule, addError, invocationCount, from, interval, timeframeEnd]);
|
||||
|
||||
return { isLoading, response, rule, setRule };
|
||||
return { isLoading, showInvocationCountWarning, response, rule, setRule };
|
||||
};
|
||||
|
|
|
@ -39,6 +39,7 @@ import type {
|
|||
ActionsStepRuleJson,
|
||||
RuleStepsFormData,
|
||||
RuleStep,
|
||||
AdvancedPreviewOptions,
|
||||
} from '../types';
|
||||
import { DataSourceType } from '../types';
|
||||
import type { FieldValueQueryBar } from '../../../../components/rules/query_bar';
|
||||
|
@ -591,6 +592,7 @@ export const formatPreviewRule = ({
|
|||
eqlOptions,
|
||||
newTermsFields,
|
||||
historyWindowSize,
|
||||
advancedOptions,
|
||||
}: {
|
||||
index: string[];
|
||||
dataViewId?: string;
|
||||
|
@ -606,6 +608,7 @@ export const formatPreviewRule = ({
|
|||
eqlOptions: EqlOptionsSelected;
|
||||
newTermsFields: string[];
|
||||
historyWindowSize: string;
|
||||
advancedOptions?: AdvancedPreviewOptions;
|
||||
}): CreateRulesSchema => {
|
||||
const defineStepData = {
|
||||
...stepDefineDefaultValue,
|
||||
|
@ -628,10 +631,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<CreateRulesSchema>(
|
||||
defineStepData,
|
||||
|
@ -639,6 +648,6 @@ export const formatPreviewRule = ({
|
|||
scheduleStepData,
|
||||
stepActionsDefaultValue
|
||||
),
|
||||
...scheduleStepData,
|
||||
...(!advancedOptions ? scheduleStepData : {}),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -241,3 +241,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;
|
||||
}
|
||||
|
|
|
@ -52,7 +52,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 { assertUnreachable } from '../../../../../common/utility_types';
|
||||
import { wrapSearchSourceClient } from './utils/wrap_search_source_client';
|
||||
|
||||
|
@ -91,15 +90,9 @@ export const previewRulesRoute = async (
|
|||
const savedObjectsClient = coreContext.savedObjects.client;
|
||||
const siemClient = (await context.securitySolution).getAppClient();
|
||||
|
||||
const timeframeEnd = request.body.timeframeEnd;
|
||||
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 }] },
|
||||
});
|
||||
|
@ -204,7 +197,7 @@ export const previewRulesRoute = async (
|
|||
isAborted = true;
|
||||
}, PREVIEW_TIMEOUT_SECONDS * 1000);
|
||||
|
||||
const startedAt = moment();
|
||||
const startedAt = moment(timeframeEnd);
|
||||
const parsedDuration = parseDuration(internalRule.schedule.interval) ?? 0;
|
||||
startedAt.subtract(moment.duration(parsedDuration * (invocationCount - 1)));
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
const { body } = await supertest
|
||||
.post(DETECTION_ENGINE_RULES_PREVIEW)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getSimplePreviewRule('', 3))
|
||||
.send(getSimplePreviewRule('', 0))
|
||||
.expect(200);
|
||||
const { logs } = getSimpleRulePreviewOutput(undefined, [
|
||||
{ errors: ['Invalid invocation count'], warnings: [], duration: 0 },
|
||||
|
|
|
@ -26,4 +26,5 @@ export const getSimplePreviewRule = (
|
|||
type: 'query',
|
||||
query: 'user.name: root or user.name: admin',
|
||||
invocationCount,
|
||||
timeframeEnd: new Date().toISOString(),
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue