Skip to content

Commit

Permalink
[Security Solution][Detections] Preview Rule: Make it possible to con…
Browse files Browse the repository at this point in the history
…figure the time interval and look-back time (elastic#4362)
  • Loading branch information
e40pud committed Jul 25, 2022
1 parent 170fa42 commit ee9d5e8
Show file tree
Hide file tree
Showing 11 changed files with 410 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 '.';
Expand Down Expand Up @@ -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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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">
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -111,6 +191,7 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
logs,
hasNoiseWarning,
isAborted,
showInvocationCountWarning,
} = usePreviewRoute({
index,
isDisabled,
Expand All @@ -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
Expand All @@ -141,8 +223,36 @@ 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 && (
<>
<EuiFormErrorText data-test-subj="previewInvocationCountWarning">
{i18n.QUERY_PREVIEW_INVOCATION_COUNT_WARNING}
</EuiFormErrorText>
<EuiSpacer />
</>
)}
<EuiFormRow
label={i18n.QUERY_PREVIEW_LABEL}
helpText={HelpTextComponent}
Expand All @@ -153,15 +263,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
Expand All @@ -176,7 +297,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
Expand All @@ -186,6 +331,7 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
addNoiseWarning={addNoiseWarning}
spaceId={spaceId}
index={index}
advancedOptions={advancedOptions}
/>
)}
<PreviewLogsComponent logs={logs} hasNoiseWarning={hasNoiseWarning} isAborted={isAborted} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
});
});
});
Loading

0 comments on commit ee9d5e8

Please sign in to comment.