Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution][Detections] Preview Rule: Make it possible to configure the time interval and look-back time #137102

Merged
merged 9 commits into from
Jul 26, 2022
Merged
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