[Security Solution][Detections] Rule Preview should process override fields and exceptions (#4680) (#140221)

* [Detections] Rule Preview should process override fields and exceptions (#4680)

* CI fixes

- Types
- Unused translations
- Unit tests

* Fix cypress tests

* Fix broken alerts table in fullscreen mode

* Update rule configuration state on about step overrides chnges

* [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix'

* Review comments

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ievgen Sorokopud 2022-09-13 21:23:05 +02:00 committed by GitHub
parent d2844f7cda
commit cbe7dc8106
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 941 additions and 934 deletions

View file

@ -170,7 +170,9 @@ export const RISK_OVERRIDE =
export const RULES_CREATION_FORM = '[data-test-subj="stepDefineRule"]';
export const RULES_CREATION_PREVIEW = '[data-test-subj="rule-preview"]';
export const RULES_CREATION_PREVIEW_BUTTON = '[data-test-subj="preview-flyout"]';
export const RULES_CREATION_PREVIEW_REFRESH_BUTTON = '[data-test-subj="previewSubmitButton"]';
export const RULE_DESCRIPTION_INPUT =
'[data-test-subj="detectionEngineStepAboutRuleDescription"] [data-test-subj="input"]';

View file

@ -65,7 +65,8 @@ import {
RULE_STATUS,
RULE_TIMESTAMP_OVERRIDE,
RULES_CREATION_FORM,
RULES_CREATION_PREVIEW,
RULES_CREATION_PREVIEW_BUTTON,
RULES_CREATION_PREVIEW_REFRESH_BUTTON,
RUNS_EVERY_INTERVAL,
RUNS_EVERY_TIME_TYPE,
SCHEDULE_CONTINUE_BUTTON,
@ -336,15 +337,13 @@ export const fillDefineEqlRuleAndContinue = (rule: CustomRule) => {
cy.get(RULES_CREATION_FORM).find(EQL_QUERY_INPUT).should('be.visible');
cy.get(RULES_CREATION_FORM).find(EQL_QUERY_INPUT).type(rule.customQuery);
cy.get(RULES_CREATION_FORM).find(EQL_QUERY_VALIDATION_SPINNER).should('not.exist');
cy.get(RULES_CREATION_PREVIEW)
.find(QUERY_PREVIEW_BUTTON)
.should('not.be.disabled')
.click({ force: true });
cy.get(RULES_CREATION_PREVIEW_BUTTON).should('not.be.disabled').click({ force: true });
cy.get(RULES_CREATION_PREVIEW_REFRESH_BUTTON).should('not.be.disabled').click({ force: true });
cy.get(PREVIEW_HISTOGRAM)
.invoke('text')
.then((text) => {
if (text !== 'Rule Preview') {
cy.get(RULES_CREATION_PREVIEW).find(QUERY_PREVIEW_BUTTON).click({ force: true });
cy.get(RULES_CREATION_PREVIEW_REFRESH_BUTTON).click({ force: true });
cy.get(PREVIEW_HISTOGRAM).should('contain.text', 'Rule Preview');
}
});

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import moment from 'moment';
import { DataSourceType } from '../../../pages/detection_engine/rules/types';
import {
isNoisy,
@ -14,51 +15,75 @@ import {
} from './helpers';
describe('query_preview/helpers', () => {
const timeframeEnd = moment();
const startHourAgo = timeframeEnd.clone().subtract(1, 'hour');
const startDayAgo = timeframeEnd.clone().subtract(1, 'day');
const startMonthAgo = timeframeEnd.clone().subtract(1, 'month');
const lastHourTimeframe = {
timeframeStart: startHourAgo,
timeframeEnd,
interval: '5m',
lookback: '1m',
};
const lastDayTimeframe = {
timeframeStart: startDayAgo,
timeframeEnd,
interval: '1h',
lookback: '5m',
};
const lastMonthTimeframe = {
timeframeStart: startMonthAgo,
timeframeEnd,
interval: '1d',
lookback: '1h',
};
describe('isNoisy', () => {
test('returns true if timeframe selection is "Last hour" and average hits per hour is greater than one execution duration', () => {
const isItNoisy = isNoisy(30, 'h');
const isItNoisy = isNoisy(30, lastHourTimeframe);
expect(isItNoisy).toBeTruthy();
});
test('returns false if timeframe selection is "Last hour" and average hits per hour is less than one execution duration', () => {
const isItNoisy = isNoisy(0, 'h');
const isItNoisy = isNoisy(0, lastHourTimeframe);
expect(isItNoisy).toBeFalsy();
});
test('returns true if timeframe selection is "Last day" and average hits per hour is greater than one execution duration', () => {
const isItNoisy = isNoisy(50, 'd');
const isItNoisy = isNoisy(50, lastDayTimeframe);
expect(isItNoisy).toBeTruthy();
});
test('returns false if timeframe selection is "Last day" and average hits per hour is equal to one execution duration', () => {
const isItNoisy = isNoisy(24, 'd');
const isItNoisy = isNoisy(24, lastDayTimeframe);
expect(isItNoisy).toBeFalsy();
});
test('returns false if timeframe selection is "Last day" and hits is 0', () => {
const isItNoisy = isNoisy(0, 'd');
const isItNoisy = isNoisy(0, lastDayTimeframe);
expect(isItNoisy).toBeFalsy();
});
test('returns true if timeframe selection is "Last month" and average hits per hour is greater than one execution duration', () => {
const isItNoisy = isNoisy(50, 'M');
const isItNoisy = isNoisy(750, lastMonthTimeframe);
expect(isItNoisy).toBeTruthy();
});
test('returns false if timeframe selection is "Last month" and average hits per hour is equal to one execution duration', () => {
const isItNoisy = isNoisy(30, 'M');
const isItNoisy = isNoisy(30, lastMonthTimeframe);
expect(isItNoisy).toBeFalsy();
});
test('returns false if timeframe selection is "Last month" and hits is 0', () => {
const isItNoisy = isNoisy(0, 'M');
const isItNoisy = isNoisy(0, lastMonthTimeframe);
expect(isItNoisy).toBeFalsy();
});

View file

@ -9,7 +9,6 @@ import { isEmpty } from 'lodash';
import { Position, ScaleType } from '@elastic/charts';
import type { EuiSelectOption } from '@elastic/eui';
import type { Type, Language, ThreatMapping } from '@kbn/securitysolution-io-ts-alerting-types';
import type { Unit } from '@kbn/datemath';
import type { Filter } from '@kbn/es-query';
import * as i18n from './translations';
import { histogramDateTimeFormatter } from '../../../../common/components/utils';
@ -17,6 +16,7 @@ import type { ChartSeriesConfigs } from '../../../../common/components/charts/co
import { getQueryFilter } from '../../../../../common/detection_engine/get_query_filter';
import type { FieldValueQueryBar } from '../query_bar';
import type { ESQuery } from '../../../../../common/typed_json';
import type { TimeframePreviewOptions } from '../../../pages/detection_engine/rules/types';
import { DataSourceType } from '../../../pages/detection_engine/rules/types';
/**
@ -25,18 +25,13 @@ import { DataSourceType } from '../../../pages/detection_engine/rules/types';
* @param hits Total query search hits
* @param timeframe Range selected by user (last hour, day...)
*/
export const isNoisy = (hits: number, timeframe: Unit): boolean => {
if (timeframe === 'h') {
return hits > 1;
} else if (timeframe === 'd') {
return hits / 24 > 1;
} else if (timeframe === 'w') {
return hits / 168 > 1;
} else if (timeframe === 'M') {
return hits / 30 > 1;
}
return false;
export const isNoisy = (hits: number, timeframe: TimeframePreviewOptions): boolean => {
const oneHour = 1000 * 60 * 60;
const durationInHours = Math.max(
(timeframe.timeframeEnd.valueOf() - timeframe.timeframeStart.valueOf()) / oneHour,
1.0
);
return hits / durationInHours > 1;
};
/**

View file

@ -7,17 +7,22 @@
import React from 'react';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import type { DataViewBase } from '@kbn/es-query';
import { fields } from '@kbn/data-plugin/common/mocks';
import { TestProviders } from '../../../../common/mock';
import type { RulePreviewProps } from '.';
import { RulePreview } from '.';
import { RulePreview, REASONABLE_INVOCATION_COUNT } from '.';
import { usePreviewRoute } from './use_preview_route';
import { usePreviewHistogram } from './use_preview_histogram';
import { DataSourceType } from '../../../pages/detection_engine/rules/types';
import {
getStepScheduleDefaultValue,
stepAboutDefaultValue,
stepDefineDefaultValue,
} from '../../../pages/detection_engine/rules/utils';
import { usePreviewInvocationCount } from '../../../containers/detection_engine/rules/use_preview_invocation_count';
jest.mock('../../../../common/lib/kibana');
jest.mock('./use_preview_route');
@ -30,6 +35,7 @@ jest.mock('../../../../common/containers/use_global_time', () => ({
setQuery: jest.fn(),
}),
}));
jest.mock('../../../containers/detection_engine/rules/use_preview_invocation_count');
const getMockIndexPattern = (): DataViewBase => ({
fields,
@ -38,42 +44,46 @@ const getMockIndexPattern = (): DataViewBase => ({
});
const defaultProps: RulePreviewProps = {
ruleType: 'threat_match',
index: ['test-*'],
indexPattern: getMockIndexPattern(),
dataSourceType: DataSourceType.IndexPatterns,
threatIndex: ['threat-*'],
threatMapping: [
{
entries: [
{ field: 'file.hash.md5', value: 'threat.indicator.file.hash.md5', type: 'mapping' },
],
defineRuleData: {
...stepDefineDefaultValue,
ruleType: 'threat_match',
index: ['test-*'],
indexPattern: getMockIndexPattern(),
dataSourceType: DataSourceType.IndexPatterns,
threatIndex: ['threat-*'],
threatMapping: [
{
entries: [
{ field: 'file.hash.md5', value: 'threat.indicator.file.hash.md5', type: 'mapping' },
],
},
],
queryBar: {
filters: [],
query: { query: 'file.hash.md5:*', language: 'kuery' },
saved_id: null,
},
],
isDisabled: false,
query: {
filters: [],
query: { query: 'file.hash.md5:*', language: 'kuery' },
saved_id: null,
},
threatQuery: {
filters: [],
query: { query: 'threat.indicator.file.hash.md5:*', language: 'kuery' },
saved_id: null,
},
threshold: {
field: ['agent.hostname'],
value: '200',
cardinality: {
field: ['user.name'],
value: '2',
threatQueryBar: {
filters: [],
query: { query: 'threat.indicator.file.hash.md5:*', language: 'kuery' },
saved_id: null,
},
threshold: {
field: ['agent.hostname'],
value: '200',
cardinality: {
field: ['user.name'],
value: '2',
},
},
anomalyThreshold: 50,
machineLearningJobId: ['test-ml-job-id'],
eqlOptions: {},
newTermsFields: ['host.ip'],
historyWindowSize: '7d',
},
anomalyThreshold: 50,
machineLearningJobId: ['test-ml-job-id'],
eqlOptions: {},
newTermsFields: ['host.ip'],
historyWindowSize: '7d',
aboutRuleData: stepAboutDefaultValue,
scheduleRuleData: getStepScheduleDefaultValue('threat_match'),
};
describe('PreviewQuery', () => {
@ -98,6 +108,8 @@ describe('PreviewQuery', () => {
isPreviewRequestInProgress: false,
previewId: undefined,
});
(usePreviewInvocationCount as jest.Mock).mockReturnValue({ invocationCount: 500 });
});
afterEach(() => {
@ -115,26 +127,6 @@ describe('PreviewQuery', () => {
expect(await wrapper.findByTestId('preview-time-frame')).toBeTruthy();
});
test('it renders preview button disabled if "isDisabled" is true', async () => {
const wrapper = render(
<TestProviders>
<RulePreview {...defaultProps} isDisabled={true} />
</TestProviders>
);
expect(await wrapper.getByTestId('queryPreviewButton').closest('button')).toBeDisabled();
});
test('it renders preview button enabled if "isDisabled" is false', async () => {
const wrapper = render(
<TestProviders>
<RulePreview {...defaultProps} />
</TestProviders>
);
expect(await wrapper.getByTestId('queryPreviewButton').closest('button')).not.toBeDisabled();
});
test('does not render histogram when there is no previewId', async () => {
const wrapper = render(
<TestProviders>
@ -145,40 +137,9 @@ 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,
test('it renders invocation count warning when invocation count is bigger then "REASONABLE_INVOCATION_COUNT"', async () => {
(usePreviewInvocationCount as jest.Mock).mockReturnValue({
invocationCount: REASONABLE_INVOCATION_COUNT + 1,
});
const wrapper = render(
@ -187,8 +148,6 @@ describe('PreviewQuery', () => {
</TestProviders>
);
const advancedQueryButton = await wrapper.findByTestId('advancedQuery');
userEvent.click(advancedQueryButton);
expect(await wrapper.findByTestId('previewInvocationCountWarning')).toBeTruthy();
});
});

View file

@ -5,53 +5,38 @@
* 2.0.
*/
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import React, { useState, useEffect, useCallback, useMemo } 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 { DataViewBase } from '@kbn/es-query';
import type { EuiButtonGroupOptionProps, OnTimeChangeProps } from '@elastic/eui';
import type { OnTimeChangeProps } from '@elastic/eui';
import {
EuiButtonGroup,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiSelect,
EuiFormRow,
EuiButton,
EuiSpacer,
EuiSuperDatePicker,
EuiSuperUpdateButton,
} 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 type { List } from '@kbn/securitysolution-io-ts-list-types';
import { isEqual } from 'lodash';
import * as i18n from './translations';
import { usePreviewRoute } from './use_preview_route';
import { PreviewHistogram } from './preview_histogram';
import { getTimeframeOptions } from './helpers';
import { PreviewLogsComponent } from './preview_logs';
import { useKibana } from '../../../../common/lib/kibana';
import { LoadingHistogram } from './loading_histogram';
import type { FieldValueThreshold } from '../threshold_input';
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,
DataSourceType,
AboutStepRule,
DefineStepRule,
ScheduleStepRule,
TimeframePreviewOptions,
} from '../../../pages/detection_engine/rules/types';
import { schema } from './schema';
import { usePreviewInvocationCount } from '../../../containers/detection_engine/rules/use_preview_invocation_count';
const HelpTextComponent = (
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem>{i18n.QUERY_PREVIEW_HELP_TEXT}</EuiFlexItem>
<EuiFlexItem>{i18n.QUERY_PREVIEW_DISCLAIMER}</EuiFlexItem>
</EuiFlexGroup>
);
export const REASONABLE_INVOCATION_COUNT = 200;
const timeRanges = [
{ start: 'now/d', end: 'now', label: 'Today' },
@ -64,42 +49,20 @@ const timeRanges = [
{ 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[];
indexPattern: DataViewBase;
isDisabled: boolean;
query: FieldValueQueryBar;
dataViewId?: string;
dataSourceType: DataSourceType;
ruleType: Type;
threatIndex: string[];
threatMapping: ThreatMapping;
threatQuery: FieldValueQueryBar;
threshold: FieldValueThreshold;
machineLearningJobId: string[];
anomalyThreshold: number;
eqlOptions: EqlOptionsSelected;
newTermsFields: string[];
historyWindowSize: string;
isDisabled?: boolean;
defineRuleData: DefineStepRule;
aboutRuleData: AboutStepRule;
scheduleRuleData: ScheduleStepRule;
exceptionsList?: List[];
}
const Select = styled(EuiSelect)`
width: ${({ theme }) => theme.eui.euiSuperDatePickerWidth};
`;
const PreviewButton = styled(EuiButton)`
margin-left: 0;
`;
const defaultTimeRange: Unit = 'h';
interface RulePreviewState {
defineRuleData?: DefineStepRule;
aboutRuleData?: AboutStepRule;
scheduleRuleData?: ScheduleStepRule;
timeframeOptions: TimeframePreviewOptions;
}
const refreshedTimeframe = (startDate: string, endDate: string) => {
return {
@ -109,25 +72,14 @@ const refreshedTimeframe = (startDate: string, endDate: string) => {
};
const RulePreviewComponent: React.FC<RulePreviewProps> = ({
index,
indexPattern,
dataViewId,
dataSourceType,
isDisabled,
query,
ruleType,
threatIndex,
threatQuery,
threatMapping,
threshold,
machineLearningJobId,
anomalyThreshold,
eqlOptions,
newTermsFields,
historyWindowSize,
defineRuleData,
aboutRuleData,
scheduleRuleData,
exceptionsList,
}) => {
const { indexPattern, ruleType } = defineRuleData;
const { spaces } = useKibana().services;
const { loading: isMlLoading, jobs } = useSecurityJobs(false);
const [spaceId, setSpaceId] = useState('');
useEffect(() => {
@ -144,107 +96,50 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
const [timeframeStart, setTimeframeStart] = useState(moment().subtract(1, 'hour'));
const [timeframeEnd, setTimeframeEnd] = useState(moment());
const [isDateRangeInvalid, setIsDateRangeInvalid] = useState(false);
useEffect(() => {
const { start, end } = refreshedTimeframe(startDate, endDate);
setTimeframeStart(start);
setTimeframeEnd(end);
}, [startDate, endDate]);
const { form } = useForm<AdvancedPreviewForm>({
defaultValue: advancedOptionsDefaultValue,
options: { stripEmptyFields: false },
schema,
// The data state that we used for the last preview results
const [previewData, setPreviewData] = useState<RulePreviewState>({
timeframeOptions: {
timeframeStart,
timeframeEnd,
interval: '5m',
lookback: '1m',
},
});
const [{ interval: formInterval, lookback: formLookback }] = useFormData<AdvancedPreviewForm>({
form,
watch: ['interval', 'lookback'],
const { invocationCount } = usePreviewInvocationCount({
timeframeOptions: {
timeframeStart,
timeframeEnd,
interval: scheduleRuleData.interval,
lookback: scheduleRuleData.from,
},
});
const showInvocationCountWarning = invocationCount > REASONABLE_INVOCATION_COUNT;
const areRelaventMlJobsRunning = useMemo(() => {
if (ruleType !== 'machine_learning') {
return true; // Don't do the expensive logic if we don't need it
}
if (isMlLoading) {
return false;
}
const selectedJobs = jobs.filter(({ id }) => machineLearningJobId.includes(id));
return selectedJobs.every((job) => isJobStarted(job.jobState, job.datafeedState));
}, [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 && formInterval && formLookback
? {
timeframeStart,
timeframeEnd,
interval: formInterval,
lookback: formLookback,
}
: undefined,
[formInterval, formLookback, showAdvancedOptions, timeframeEnd, timeframeStart]
);
const [timeFrame, setTimeFrame] = useState<Unit>(defaultTimeRange);
const {
addNoiseWarning,
createPreview,
clearPreview,
isPreviewRequestInProgress,
previewId,
logs,
hasNoiseWarning,
isAborted,
showInvocationCountWarning,
} = usePreviewRoute({
index,
isDisabled,
dataViewId,
dataSourceType,
query,
threatIndex,
threatQuery,
timeFrame,
ruleType,
threatMapping,
threshold,
machineLearningJobId,
anomalyThreshold,
eqlOptions,
newTermsFields,
historyWindowSize,
advancedOptions,
defineRuleData: previewData.defineRuleData,
aboutRuleData: previewData.aboutRuleData,
scheduleRuleData: previewData.scheduleRuleData,
exceptionsList,
timeframeOptions: previewData.timeframeOptions,
});
// Resets the timeFrame to default when rule type is changed because not all time frames are supported by all rule types
useEffect(() => {
setTimeFrame(defaultTimeRange);
}, [ruleType]);
const { startTransaction } = useStartTransaction();
const [isRefreshing, setIsRefreshing] = useState(false);
@ -256,21 +151,15 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
setIsRefreshing(false);
}, [isRefreshing, createPreview]);
const handlePreviewClick = useCallback(() => {
startTransaction({ name: SINGLE_RULE_ACTIONS.PREVIEW });
if (showAdvancedOptions) {
// Refresh timeframe on Preview button click to make sure that relative times recalculated based on current time
const { start, end } = refreshedTimeframe(startDate, endDate);
setTimeframeStart(start);
setTimeframeEnd(end);
} else {
clearPreview();
}
setIsRefreshing(true);
}, [clearPreview, endDate, showAdvancedOptions, startDate, startTransaction]);
useEffect(() => {
const { start, end } = refreshedTimeframe(startDate, endDate);
setTimeframeStart(start);
setTimeframeEnd(end);
}, [endDate, startDate]);
const onTimeChange = useCallback(
({ start: newStart, end: newEnd, isInvalid }: OnTimeChangeProps) => {
setIsDateRangeInvalid(isInvalid);
if (!isInvalid) {
setStartDate(newStart);
setEndDate(newEnd);
@ -279,18 +168,50 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
[]
);
const onTimeframeRefresh = useCallback(() => {
startTransaction({ name: SINGLE_RULE_ACTIONS.PREVIEW });
const { start, end } = refreshedTimeframe(startDate, endDate);
setTimeframeStart(start);
setTimeframeEnd(end);
setPreviewData({
defineRuleData,
aboutRuleData,
scheduleRuleData,
timeframeOptions: {
timeframeStart: start,
timeframeEnd: end,
interval: scheduleRuleData.interval,
lookback: scheduleRuleData.from,
},
});
setIsRefreshing(true);
}, [aboutRuleData, defineRuleData, endDate, scheduleRuleData, startDate, startTransaction]);
const isDirty = useMemo(
() =>
!timeframeStart.isSame(previewData.timeframeOptions.timeframeStart) ||
!timeframeEnd.isSame(previewData.timeframeOptions.timeframeEnd) ||
!isEqual(defineRuleData, previewData.defineRuleData) ||
!isEqual(aboutRuleData, previewData.aboutRuleData) ||
!isEqual(scheduleRuleData, previewData.scheduleRuleData),
[
aboutRuleData,
defineRuleData,
previewData.aboutRuleData,
previewData.defineRuleData,
previewData.scheduleRuleData,
previewData.timeframeOptions.timeframeEnd,
previewData.timeframeOptions.timeframeStart,
scheduleRuleData,
timeframeEnd,
timeframeStart,
]
);
return (
<>
<EuiButtonGroup
legend="Quick query or advanced query preview selector"
data-test-subj="quickAdvancedToggleButtonGroup"
idSelected={queryPreviewIdSelected}
onChange={onChangeDataSource}
options={quickAdvancedToggleButtonOptions}
color="primary"
/>
<EuiSpacer size="s" />
{showAdvancedOptions && showInvocationCountWarning && (
{showInvocationCountWarning && (
<>
<EuiCallOut
color="warning"
@ -304,83 +225,44 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
)}
<EuiFormRow
label={i18n.QUERY_PREVIEW_LABEL}
helpText={HelpTextComponent}
error={undefined}
isInvalid={false}
data-test-subj="rule-preview"
describedByIds={['rule-preview']}
>
<EuiFlexGroup>
<EuiFlexItem grow={1}>
{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>
<EuiFlexGroup alignItems="center" responsive={false} gutterSize="s">
<EuiSuperDatePicker
start={startDate}
end={endDate}
isDisabled={isDisabled}
onTimeChange={onTimeChange}
showUpdateButton={false}
commonlyUsedRanges={timeRanges}
onRefresh={onTimeframeRefresh}
data-test-subj="preview-time-frame"
/>
<EuiFlexItem grow={false}>
<PreviewButton
fill
isLoading={isPreviewRequestInProgress}
isDisabled={isDisabled || !areRelaventMlJobsRunning}
onClick={handlePreviewClick}
data-test-subj="queryPreviewButton"
>
{i18n.QUERY_PREVIEW_BUTTON}
</PreviewButton>
<EuiSuperUpdateButton
isDisabled={isDateRangeInvalid || isDisabled}
iconType={isDirty ? 'kqlFunction' : 'refresh'}
onClick={onTimeframeRefresh}
color={isDirty ? 'success' : 'primary'}
fill={true}
data-test-subj="previewSubmitButton"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
{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>
)}
<EuiSpacer size="l" />
{isPreviewRequestInProgress && <LoadingHistogram />}
{!isPreviewRequestInProgress && previewId && spaceId && (
<PreviewHistogram
ruleType={ruleType}
timeFrame={timeFrame}
previewId={previewId}
addNoiseWarning={addNoiseWarning}
spaceId={spaceId}
indexPattern={indexPattern}
advancedOptions={advancedOptions}
timeframeOptions={previewData.timeframeOptions}
/>
)}
<PreviewLogsComponent logs={logs} hasNoiseWarning={hasNoiseWarning} isAborted={isAborted} />

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer, EuiLoadingChart } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiLoadingChart } from '@elastic/eui';
import styled from 'styled-components';
import * as i18n from './translations';
import { Panel } from '../../../../common/components/panel';
@ -29,14 +29,7 @@ export const LoadingHistogram = () => {
<EuiFlexItem grow={1}>
<LoadingChart size="l" data-test-subj="preview-histogram-loading" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<>
<EuiSpacer />
<EuiText size="s" color="subdued">
<p>{i18n.QUERY_PREVIEW_DISCLAIMER}</p>
</EuiText>
</>
</EuiFlexItem>
<EuiSpacer />
</EuiFlexGroup>
</Panel>
);

View file

@ -30,6 +30,13 @@ const getMockIndexPattern = (): DataViewBase => ({
title: 'logstash-*',
});
const getLastMonthTimeframe = () => ({
timeframeStart: moment().subtract(1, 'month'),
timeframeEnd: moment(),
interval: '5m',
lookback: '1m',
});
describe('PreviewHistogram', () => {
const mockSetQuery = jest.fn();
@ -63,7 +70,7 @@ describe('PreviewHistogram', () => {
<TestProviders>
<PreviewHistogram
addNoiseWarning={jest.fn()}
timeFrame="M"
timeframeOptions={getLastMonthTimeframe()}
previewId={'test-preview-id'}
spaceId={'default'}
ruleType={'query'}
@ -94,7 +101,7 @@ describe('PreviewHistogram', () => {
<TestProviders>
<PreviewHistogram
addNoiseWarning={jest.fn()}
timeFrame="M"
timeframeOptions={getLastMonthTimeframe()}
previewId={'test-preview-id'}
spaceId={'default'}
ruleType={'query'}
@ -146,12 +153,11 @@ describe('PreviewHistogram', () => {
<TestProviders>
<PreviewHistogram
addNoiseWarning={jest.fn()}
timeFrame="M"
previewId={'test-preview-id'}
spaceId={'default'}
ruleType={'query'}
indexPattern={getMockIndexPattern()}
advancedOptions={{
timeframeOptions={{
timeframeStart: moment(start, format),
timeframeEnd: moment(end, format),
interval: '5m',

View file

@ -7,7 +7,6 @@
import React, { useEffect, useMemo } from 'react';
import usePrevious from 'react-use/lib/usePrevious';
import type { Unit } from '@kbn/datemath';
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer, EuiLoadingChart } from '@elastic/eui';
import styled from 'styled-components';
import type { Type } from '@kbn/securitysolution-io-ts-alerting-types';
@ -27,7 +26,6 @@ import { Panel } from '../../../../common/components/panel';
import { HeaderSection } from '../../../../common/components/header_section';
import { BarChart } from '../../../../common/components/charts/barchart';
import { usePreviewHistogram } from './use_preview_histogram';
import { formatDate } from '../../../../common/components/super_date_picker';
import { alertsPreviewDefaultModel } from '../../alerts_table/default_config';
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
import { defaultRowRenderers } from '../../../../timelines/components/timeline/body/renderers';
@ -42,7 +40,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';
import type { TimeframePreviewOptions } from '../../../pages/detection_engine/rules/types';
const LoadingChart = styled(EuiLoadingChart)`
display: block;
@ -59,39 +57,32 @@ const FullScreenContainer = styled.div<{ $isFullScreen: boolean }>`
export const ID = 'previewHistogram';
interface PreviewHistogramProps {
timeFrame: Unit;
previewId: string;
addNoiseWarning: () => void;
spaceId: string;
ruleType: Type;
indexPattern: DataViewBase;
advancedOptions?: AdvancedPreviewOptions;
indexPattern: DataViewBase | undefined;
timeframeOptions: TimeframePreviewOptions;
}
const DEFAULT_HISTOGRAM_HEIGHT = 300;
export const PreviewHistogram = ({
timeFrame,
previewId,
addNoiseWarning,
spaceId,
ruleType,
indexPattern,
advancedOptions,
timeframeOptions,
}: 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(
() => (advancedOptions ? advancedOptions.timeframeStart.toISOString() : formatDate(from)),
[from, advancedOptions]
);
const endDate = useMemo(
() => (advancedOptions ? advancedOptions.timeframeEnd.toISOString() : formatDate(to)),
[to, advancedOptions]
() => timeframeOptions.timeframeStart.toISOString(),
[timeframeOptions]
);
const endDate = useMemo(() => timeframeOptions.timeframeEnd.toISOString(), [timeframeOptions]);
const isEqlRule = useMemo(() => ruleType === 'eql', [ruleType]);
const isMlRule = useMemo(() => ruleType === 'machine_learning', [ruleType]);
@ -133,11 +124,11 @@ export const PreviewHistogram = ({
useEffect(() => {
if (previousPreviewId !== previewId && totalCount > 0) {
if (isNoisy(totalCount, timeFrame)) {
if (isNoisy(totalCount, timeframeOptions)) {
addNoiseWarning();
}
}
}, [totalCount, addNoiseWarning, timeFrame, previousPreviewId, previewId]);
}, [totalCount, addNoiseWarning, previousPreviewId, previewId, timeframeOptions]);
useEffect((): void => {
if (!isLoading && !isInitializing) {

View file

@ -1,43 +0,0 @@
/*
* 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.',
}
),
},
};

View file

@ -61,14 +61,7 @@ export const QUERY_PREVIEW_SELECT_ARIA = i18n.translate(
export const QUERY_PREVIEW_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewLabel',
{
defaultMessage: 'Timeframe',
}
);
export const QUERY_PREVIEW_HELP_TEXT = i18n.translate(
'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewHelpText',
{
defaultMessage: 'Select a timeframe of data to preview query results.',
defaultMessage: 'Select a preview timeframe',
}
);
@ -115,14 +108,6 @@ export const QUERY_PREVIEW_ERROR = i18n.translate(
}
);
export const QUERY_PREVIEW_DISCLAIMER = i18n.translate(
'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewDisclaimer',
{
defaultMessage:
'Note: This preview excludes effects of rule exceptions and timestamp overrides.',
}
);
export const PREVIEW_HISTOGRAM_DISCLAIMER = i18n.translate(
'xpack.securitySolution.detectionEngine.queryPreview.histogramDisclaimer',
{

View file

@ -21,7 +21,7 @@ interface PreviewHistogramParams {
startDate: string;
spaceId: string;
ruleType: Type;
indexPattern: DataViewBase;
indexPattern: DataViewBase | undefined;
}
export const usePreviewHistogram = ({

View file

@ -5,80 +5,37 @@
* 2.0.
*/
import { useEffect, useMemo, useState, useCallback } from 'react';
import moment from 'moment';
import type { Unit } from '@kbn/datemath';
import type { Type, ThreatMapping } from '@kbn/securitysolution-io-ts-alerting-types';
import type { FieldValueQueryBar } from '../query_bar';
import { useEffect, useState, useCallback } from 'react';
import type { List } from '@kbn/securitysolution-io-ts-list-types';
import { usePreviewRule } from '../../../containers/detection_engine/rules/use_preview_rule';
import { formatPreviewRule } from '../../../pages/detection_engine/rules/create/helpers';
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,
DataSourceType,
AboutStepRule,
DefineStepRule,
ScheduleStepRule,
TimeframePreviewOptions,
} from '../../../pages/detection_engine/rules/types';
interface PreviewRouteParams {
isDisabled: boolean;
index: string[];
dataViewId?: string;
dataSourceType: DataSourceType;
threatIndex: string[];
query: FieldValueQueryBar;
threatQuery: FieldValueQueryBar;
ruleType: Type;
timeFrame: Unit;
threatMapping: ThreatMapping;
threshold: FieldValueThreshold;
machineLearningJobId: string[];
anomalyThreshold: number;
eqlOptions: EqlOptionsSelected;
newTermsFields: string[];
historyWindowSize: string;
advancedOptions?: AdvancedPreviewOptions;
defineRuleData?: DefineStepRule;
aboutRuleData?: AboutStepRule;
scheduleRuleData?: ScheduleStepRule;
exceptionsList?: List[];
timeframeOptions: TimeframePreviewOptions;
}
export const usePreviewRoute = ({
index,
dataViewId,
dataSourceType,
isDisabled,
query,
threatIndex,
threatQuery,
timeFrame,
ruleType,
threatMapping,
threshold,
machineLearningJobId,
anomalyThreshold,
eqlOptions,
newTermsFields,
historyWindowSize,
advancedOptions,
defineRuleData,
aboutRuleData,
scheduleRuleData,
exceptionsList,
timeframeOptions,
}: PreviewRouteParams) => {
const [isRequestTriggered, setIsRequestTriggered] = useState(false);
const [timeframeEnd, setTimeframeEnd] = useState(moment());
useEffect(() => {
if (isRequestTriggered) {
setTimeframeEnd(moment());
}
}, [isRequestTriggered, setTimeframeEnd]);
const quickQueryOptions = useMemo(
() => ({
timeframe: timeFrame,
timeframeEnd,
}),
[timeFrame, timeframeEnd]
);
const { isLoading, showInvocationCountWarning, response, rule, setRule } = usePreviewRule({
quickQueryOptions,
advancedOptions,
const { isLoading, response, rule, setRule } = usePreviewRule({
timeframeOptions,
});
const [logs, setLogs] = useState<RulePreviewLogs[]>(response.logs ?? []);
const [isAborted, setIsAborted] = useState<boolean>(!!response.isAborted);
@ -102,69 +59,27 @@ export const usePreviewRoute = ({
}, [setRule]);
useEffect(() => {
clearPreview();
}, [
clearPreview,
index,
isDisabled,
query,
threatIndex,
threatQuery,
timeFrame,
ruleType,
threatMapping,
threshold,
machineLearningJobId,
anomalyThreshold,
eqlOptions,
newTermsFields,
historyWindowSize,
advancedOptions,
]);
useEffect(() => {
if (!defineRuleData || !aboutRuleData || !scheduleRuleData) {
return;
}
if (isRequestTriggered && rule === null) {
setRule(
formatPreviewRule({
index,
dataViewId,
dataSourceType,
query,
ruleType,
threatIndex,
threatMapping,
threatQuery,
timeFrame,
threshold,
machineLearningJobId,
anomalyThreshold,
eqlOptions,
newTermsFields,
historyWindowSize,
advancedOptions,
defineRuleData,
aboutRuleData,
scheduleRuleData,
exceptionsList,
})
);
}
}, [
index,
dataViewId,
dataSourceType,
isRequestTriggered,
query,
rule,
ruleType,
setRule,
threatIndex,
threatMapping,
threatQuery,
timeFrame,
threshold,
machineLearningJobId,
anomalyThreshold,
eqlOptions,
newTermsFields,
historyWindowSize,
advancedOptions,
defineRuleData,
aboutRuleData,
scheduleRuleData,
exceptionsList,
]);
return {
@ -176,6 +91,5 @@ export const usePreviewRoute = ({
previewId: response.previewId ?? '',
logs,
isAborted,
showInvocationCountWarning,
};
};

View file

@ -11,6 +11,7 @@ import React, { memo, useCallback, useEffect, useState, useMemo } from 'react';
import styled from 'styled-components';
import type { DataViewBase } from '@kbn/es-query';
import { isThreatMatchRule } from '../../../../../common/detection_engine/utils';
import type {
RuleStepProps,
AboutStepRule,
@ -31,7 +32,6 @@ import {
} from '../../../../shared_imports';
import { defaultRiskScoreBySeverity, severityOptions } from './data';
import { stepAboutDefaultValue } from './default_value';
import { isUrlInvalid } from '../../../../common/utils/validators';
import { schema as defaultSchema, threatIndicatorPathRequiredSchemaValue } from './schema';
import * as I18n from './translations';
@ -42,7 +42,6 @@ import { SeverityField } from '../severity_mapping';
import { RiskScoreField } from '../risk_score_mapping';
import { AutocompleteField } from '../autocomplete_field';
import { useFetchIndex } from '../../../../common/containers/source';
import { isThreatMatchRule } from '../../../../../common/detection_engine/utils';
import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../../../common/constants';
import { useKibana } from '../../../../common/lib/kibana';
import { useRuleIndices } from '../../../containers/detection_engine/rules/use_rule_indices';
@ -50,8 +49,9 @@ import { useRuleIndices } from '../../../containers/detection_engine/rules/use_r
const CommonUseField = getUseField({ component: Field });
interface StepAboutRuleProps extends RuleStepProps {
defaultValues?: AboutStepRule;
defaultValues: AboutStepRule;
defineRuleData?: DefineStepRule;
onRuleDataChange?: (data: AboutStepRule) => void;
}
const ThreeQuartersContainer = styled.div`
@ -68,7 +68,7 @@ TagContainer.displayName = 'TagContainer';
const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({
addPadding = false,
defaultValues,
defaultValues: initialState,
defineRuleData,
descriptionColumns = 'singleSplit',
isReadOnlyView,
@ -76,21 +76,13 @@ const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({
isLoading,
onSubmit,
setForm,
onRuleDataChange,
}) => {
const { data } = useKibana().services;
const isThreatMatchRuleValue = useMemo(
() => isThreatMatchRule(defineRuleData?.ruleType),
[defineRuleData?.ruleType]
);
const initialState: AboutStepRule = useMemo(
() =>
defaultValues ??
(isThreatMatchRuleValue
? { ...stepAboutDefaultValue, threatIndicatorPath: DEFAULT_INDICATOR_SOURCE_PATH }
: stepAboutDefaultValue),
[defaultValues, isThreatMatchRuleValue]
[defineRuleData]
);
const schema = useMemo(
@ -147,7 +139,25 @@ const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({
const [{ severity: formSeverity, timestampOverride: formTimestampOverride }] =
useFormData<AboutStepRule>({
form,
watch: ['severity', 'timestampOverride'],
watch: [
'isAssociatedToEndpointList',
'isBuildingBlock',
'riskScore',
'ruleNameOverride',
'severity',
'timestampOverride',
'threat',
'timestampOverrideFallbackDisabled',
],
onChange: (aboutData: AboutStepRule) => {
if (onRuleDataChange) {
onRuleDataChange({
threatIndicatorPath: undefined,
timestampOverrideFallbackDisabled: undefined,
...aboutData,
});
}
},
});
useEffect(() => {
@ -166,7 +176,14 @@ const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({
const getData = useCallback(async () => {
const result = await submit();
return result?.isValid
? result
? {
isValid: true,
data: {
threatIndicatorPath: undefined,
timestampOverrideFallbackDisabled: undefined,
...result.data,
},
}
: {
isValid: false,
data: getFormData(),

View file

@ -9,6 +9,7 @@ import React from 'react';
import { shallow } from 'enzyme';
import { StepDefineRule, aggregatableFields } from '.';
import { stepDefineDefaultValue } from '../../../pages/detection_engine/rules/utils';
jest.mock('../../../../common/lib/kibana');
jest.mock('../../../../common/hooks/use_selector', () => {
@ -84,7 +85,15 @@ test('aggregatableFields with aggregatable: true', function () {
describe('StepDefineRule', () => {
it('renders correctly', () => {
const wrapper = shallow(<StepDefineRule isReadOnlyView={false} isLoading={false} />);
const wrapper = shallow(
<StepDefineRule
isReadOnlyView={false}
isLoading={false}
indicesConfig={[]}
threatIndicesConfig={[]}
defaultValues={stepDefineDefaultValue}
/>
);
expect(wrapper.find('Form[data-test-subj="stepDefineRule"]')).toHaveLength(1);
});

View file

@ -26,17 +26,11 @@ import usePrevious from 'react-use/lib/usePrevious';
import type { DataViewBase } from '@kbn/es-query';
import { FormattedMessage } from '@kbn/i18n-react';
import {
DEFAULT_INDEX_KEY,
DEFAULT_THREAT_INDEX_KEY,
DEFAULT_THREAT_MATCH_QUERY,
} from '../../../../../common/constants';
import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations';
import { isMlRule } from '../../../../../common/machine_learning/helpers';
import { hasMlAdminPermissions } from '../../../../../common/machine_learning/has_ml_admin_permissions';
import { hasMlLicense } from '../../../../../common/machine_learning/has_ml_license';
import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml_capabilities';
import { useUiSetting$, useKibana } from '../../../../common/lib/kibana';
import { useKibana } from '../../../../common/lib/kibana';
import type { EqlOptionsSelected, FieldsEqlOptions } from '../../../../../common/search_strategy';
import {
filterRuleFieldsForType,
@ -75,12 +69,12 @@ import { DataViewSelector } from '../data_view_selector';
import { ThreatMatchInput } from '../threatmatch_input';
import type { BrowserField } from '../../../../common/containers/source';
import { useFetchIndex } from '../../../../common/containers/source';
import { RulePreview } from '../rule_preview';
import { getIsRulePreviewDisabled } from '../rule_preview/helpers';
import { NewTermsFields } from '../new_terms_fields';
import { ScheduleItem } from '../schedule_item_form';
import { DocLink } from '../../../../common/components/links_to_docs/doc_link';
import { StepDefineRuleNewFeaturesTour } from './new_features_tour';
import { defaultCustomQuery } from '../../../pages/detection_engine/rules/utils';
import { getIsRulePreviewDisabled } from '../rule_preview/helpers';
const CommonUseField = getUseField({ component: Field });
@ -88,62 +82,13 @@ const StyledVisibleContainer = styled.div<{ isVisible: boolean }>`
display: ${(props) => (props.isVisible ? 'block' : 'none')};
`;
interface StepDefineRuleProps extends RuleStepProps {
defaultValues?: DefineStepRule;
indicesConfig: string[];
threatIndicesConfig: string[];
defaultValues: DefineStepRule;
onRuleDataChange?: (data: DefineStepRule) => void;
onPreviewDisabledStateChange?: (isDisabled: boolean) => void;
}
export const stepDefineDefaultValue: DefineStepRule = {
anomalyThreshold: 50,
index: [],
machineLearningJobId: [],
ruleType: 'query',
threatIndex: [],
queryBar: {
query: { query: '', language: 'kuery' },
filters: [],
saved_id: null,
},
threatQueryBar: {
query: { query: DEFAULT_THREAT_MATCH_QUERY, language: 'kuery' },
filters: [],
saved_id: null,
},
requiredFields: [],
relatedIntegrations: [],
threatMapping: [],
threshold: {
field: [],
value: '200',
cardinality: {
field: [],
value: '',
},
},
timeline: {
id: null,
title: DEFAULT_TIMELINE_TITLE,
},
eqlOptions: {},
dataSourceType: DataSourceType.IndexPatterns,
newTermsFields: [],
historyWindowSize: '7d',
};
/**
* This default query will be used for threat query/indicator matches
* as the default when the user swaps to using it by changing their
* rule type from any rule type to the "threatMatchRule" type. Only
* difference is that "*:*" is used instead of '' for its query.
*/
const threatQueryBarDefaultValue: DefineStepRule['queryBar'] = {
...stepDefineDefaultValue.queryBar,
query: { ...stepDefineDefaultValue.queryBar.query, query: '*:*' },
};
const defaultCustomQuery = {
forNormalRules: stepDefineDefaultValue.queryBar,
forThreatMatchRules: threatQueryBarDefaultValue,
};
export const MyLabelButton = styled(EuiButtonEmpty)`
height: 18px;
font-size: 12px;
@ -166,7 +111,7 @@ const RuleTypeEuiFormRow = styled(EuiFormRow).attrs<{ $isVisible: boolean }>(({
const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
addPadding = false,
defaultValues,
defaultValues: initialState,
descriptionColumns = 'singleSplit',
isReadOnlyView,
isLoading,
@ -174,6 +119,10 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
onSubmit,
setForm,
kibanaDataViews,
indicesConfig,
threatIndicesConfig,
onRuleDataChange,
onPreviewDisabledStateChange,
}) => {
const mlCapabilities = useMlCapabilities();
const [openTimelineSearch, setOpenTimelineSearch] = useState(false);
@ -181,14 +130,6 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
const [threatIndexModified, setThreatIndexModified] = useState(false);
const [dataViewTitle, setDataViewTitle] = useState<string>();
const [indicesConfig] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY);
const [threatIndicesConfig] = useUiSetting$<string[]>(DEFAULT_THREAT_INDEX_KEY);
const initialState = defaultValues ?? {
...stepDefineDefaultValue,
index: indicesConfig,
threatIndex: threatIndicesConfig,
};
const { form } = useForm<DefineStepRule>({
defaultValue: initialState,
options: { stripEmptyFields: false },
@ -196,23 +137,7 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
});
const { getFields, getFormData, reset, submit } = form;
const [
{
index: formIndex,
ruleType: formRuleType,
queryBar: formQuery,
dataViewId: formDataViewId,
threatIndex: formThreatIndex,
threatQueryBar: formThreatQuery,
threshold: formThreshold,
threatMapping: formThreatMapping,
machineLearningJobId: formMachineLearningJobId,
anomalyThreshold: formAnomalyThreshold,
dataSourceType: formDataSourceType,
newTermsFields: formNewTermsFields,
historyWindowSize: formHistoryWindowSize,
},
] = useFormData<DefineStepRule>({
const [formData] = useFormData<DefineStepRule>({
form,
watch: [
'index',
@ -232,25 +157,77 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
'newTermsFields',
'historyWindowSize',
],
onChange: (data: DefineStepRule) => {
if (onRuleDataChange) {
onRuleDataChange({
...data,
eqlOptions: optionsSelected,
});
}
},
});
const {
index: formIndex,
ruleType: formRuleType,
queryBar: formQuery,
dataViewId: formDataViewId,
threatIndex: formThreatIndex,
threatMapping: formThreatMapping,
machineLearningJobId: formMachineLearningJobId,
dataSourceType: formDataSourceType,
newTermsFields: formNewTermsFields,
} = formData;
const [isQueryBarValid, setIsQueryBarValid] = useState(false);
const [isThreatQueryBarValid, setIsThreatQueryBarValid] = useState(false);
const index = formIndex || initialState.index;
const dataView = formDataViewId || initialState.dataViewId;
const threatIndex = formThreatIndex || initialState.threatIndex;
const machineLearningJobId = formMachineLearningJobId ?? initialState.machineLearningJobId;
const anomalyThreshold = formAnomalyThreshold ?? initialState.anomalyThreshold;
const newTermsFields = formNewTermsFields ?? initialState.newTermsFields;
const historyWindowSize = formHistoryWindowSize ?? initialState.historyWindowSize;
const ruleType = formRuleType || initialState.ruleType;
const dataSourceType = formDataSourceType || initialState.dataSourceType;
const machineLearningJobId = formMachineLearningJobId ?? initialState.machineLearningJobId;
const [isPreviewValid, setIsPreviewValid] = useState(false);
useEffect(() => {
if (onPreviewDisabledStateChange) {
onPreviewDisabledStateChange(!isPreviewValid);
}
}, [isPreviewValid, onPreviewDisabledStateChange]);
useEffect(() => {
const isDisabled = getIsRulePreviewDisabled({
ruleType,
isQueryBarValid,
isThreatQueryBarValid,
index,
dataViewId: formDataViewId,
dataSourceType,
threatIndex,
threatMapping: formThreatMapping,
machineLearningJobId,
queryBar: formQuery ?? initialState.queryBar,
newTermsFields: formNewTermsFields,
});
setIsPreviewValid(!isDisabled);
}, [
dataSourceType,
formDataViewId,
formNewTermsFields,
formQuery,
formThreatMapping,
index,
initialState.queryBar,
isQueryBarValid,
isThreatQueryBarValid,
machineLearningJobId,
ruleType,
threatIndex,
]);
// if 'index' is selected, use these browser fields
// otherwise use the dataview browserfields
const previousRuleType = usePrevious(ruleType);
const [optionsSelected, setOptionsSelected] = useState<EqlOptionsSelected>(
defaultValues?.eqlOptions || {}
initialState.eqlOptions || {}
);
const [isIndexPatternLoading, { browserFields, indexPatterns: initIndexPattern }] = useFetchIndex(
index,
@ -836,39 +813,6 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
}}
/>
</Form>
<EuiSpacer size="m" />
<RuleTypeEuiFormRow label={i18n.RULE_PREVIEW_TITLE} $isVisible={true} fullWidth>
<RulePreview
index={index}
indexPattern={indexPattern}
dataViewId={formDataViewId}
dataSourceType={dataSourceType}
isDisabled={getIsRulePreviewDisabled({
ruleType,
isQueryBarValid,
isThreatQueryBarValid,
index,
dataViewId: formDataViewId,
dataSourceType,
threatIndex,
threatMapping: formThreatMapping,
machineLearningJobId,
queryBar: formQuery ?? initialState.queryBar,
newTermsFields: formNewTermsFields,
})}
query={formQuery}
ruleType={ruleType}
threatIndex={threatIndex}
threatQuery={formThreatQuery}
threatMapping={formThreatMapping}
threshold={formThreshold}
machineLearningJobId={machineLearningJobId}
anomalyThreshold={anomalyThreshold}
eqlOptions={optionsSelected}
newTermsFields={newTermsFields}
historyWindowSize={historyWindowSize}
/>
</RuleTypeEuiFormRow>
</StepContentWrapper>
{!isUpdateView && (

View file

@ -10,18 +10,32 @@ import { shallow, mount } from 'enzyme';
import { TestProviders } from '../../../../common/mock';
import { StepScheduleRule } from '.';
import { getStepScheduleDefaultValue } from '../../../pages/detection_engine/rules/utils';
describe('StepScheduleRule', () => {
it('renders correctly', () => {
const wrapper = mount(<StepScheduleRule isReadOnlyView={false} isLoading={false} />, {
wrappingComponent: TestProviders,
});
const wrapper = mount(
<StepScheduleRule
isReadOnlyView={false}
isLoading={false}
defaultValues={getStepScheduleDefaultValue('query')}
/>,
{
wrappingComponent: TestProviders,
}
);
expect(wrapper.find('Form[data-test-subj="stepScheduleRule"]')).toHaveLength(1);
});
it('renders correctly if isReadOnlyView', () => {
const wrapper = shallow(<StepScheduleRule isReadOnlyView={true} isLoading={false} />);
const wrapper = shallow(
<StepScheduleRule
isReadOnlyView={true}
isLoading={false}
defaultValues={getStepScheduleDefaultValue('query')}
/>
);
expect(wrapper.find('StepContentWrapper')).toHaveLength(1);
});

View file

@ -7,48 +7,32 @@
import type { FC } from 'react';
import React, { memo, useCallback, useEffect } from 'react';
import type { Type } from '@kbn/securitysolution-io-ts-alerting-types';
import type { RuleStepProps, ScheduleStepRule } from '../../../pages/detection_engine/rules/types';
import { RuleStep } from '../../../pages/detection_engine/rules/types';
import { StepRuleDescription } from '../description_step';
import { ScheduleItem } from '../schedule_item_form';
import { Form, UseField, useForm } from '../../../../shared_imports';
import { Form, UseField, useForm, useFormData } from '../../../../shared_imports';
import { StepContentWrapper } from '../step_content_wrapper';
import { isThreatMatchRule } from '../../../../../common/detection_engine/utils';
import { NextStep } from '../next_step';
import { schema } from './schema';
interface StepScheduleRuleProps extends RuleStepProps {
defaultValues?: ScheduleStepRule | null;
ruleType?: Type;
defaultValues: ScheduleStepRule;
onRuleDataChange?: (data: ScheduleStepRule) => void;
}
const DEFAULT_INTERVAL = '5m';
const DEFAULT_FROM = '1m';
const THREAT_MATCH_INTERVAL = '1h';
const THREAT_MATCH_FROM = '5m';
const getStepScheduleDefaultValue = (ruleType: Type | undefined): ScheduleStepRule => {
return {
interval: isThreatMatchRule(ruleType) ? THREAT_MATCH_INTERVAL : DEFAULT_INTERVAL,
from: isThreatMatchRule(ruleType) ? THREAT_MATCH_FROM : DEFAULT_FROM,
};
};
const StepScheduleRuleComponent: FC<StepScheduleRuleProps> = ({
addPadding = false,
defaultValues,
defaultValues: initialState,
descriptionColumns = 'singleSplit',
isReadOnlyView,
isLoading,
isUpdateView = false,
onSubmit,
setForm,
ruleType,
onRuleDataChange,
}) => {
const initialState = defaultValues ?? getStepScheduleDefaultValue(ruleType);
const { form } = useForm<ScheduleStepRule>({
defaultValue: initialState,
options: { stripEmptyFields: false },
@ -57,6 +41,16 @@ const StepScheduleRuleComponent: FC<StepScheduleRuleProps> = ({
const { getFormData, submit } = form;
useFormData<ScheduleStepRule>({
form,
watch: ['from', 'interval'],
onChange: (data: ScheduleStepRule) => {
if (onRuleDataChange) {
onRuleDataChange(data);
}
},
});
const handleSubmit = useCallback(() => {
if (onSubmit) {
onSubmit();

View file

@ -34,7 +34,7 @@ interface ThreatMatchInputProps {
threatIndexPatternsLoading: boolean;
threatIndexModified: boolean;
handleResetThreatIndices: () => void;
onValidityChange: (isValid: boolean) => void;
onValidityChange?: (isValid: boolean) => void;
}
const ThreatMatchInputComponent: React.FC<ThreatMatchInputProps> = ({
@ -54,7 +54,9 @@ const ThreatMatchInputComponent: React.FC<ThreatMatchInputProps> = ({
const [isThreatIndexPatternValid, setIsThreatIndexPatternValid] = useState(false);
useEffect(() => {
onValidityChange(!isThreatMappingInvalid && isThreatIndexPatternValid);
if (onValidityChange) {
onValidityChange(!isThreatMappingInvalid && isThreatIndexPatternValid);
}
}, [isThreatIndexPatternValid, isThreatMappingInvalid, onValidityChange]);
const handleBuilderOnChange = useCallback(

View file

@ -0,0 +1,36 @@
/*
* 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.
*/
import moment from 'moment';
import type { TimeframePreviewOptions } from '../../../pages/detection_engine/rules/types';
import { getTimeTypeValue } from '../../../pages/detection_engine/rules/create/helpers';
export const usePreviewInvocationCount = ({
timeframeOptions,
}: {
timeframeOptions: TimeframePreviewOptions;
}) => {
const timeframeDuration =
(timeframeOptions.timeframeEnd.valueOf() / 1000 -
timeframeOptions.timeframeStart.valueOf() / 1000) *
1000;
const { unit: intervalUnit, value: intervalValue } = getTimeTypeValue(timeframeOptions.interval);
const duration = moment.duration(intervalValue, intervalUnit);
const ruleIntervalDuration = duration.asMilliseconds();
const invocationCount = Math.max(Math.ceil(timeframeDuration / ruleIntervalDuration), 1);
const interval = timeframeOptions.interval;
const { unit: lookbackUnit, value: lookbackValue } = getTimeTypeValue(timeframeOptions.lookback);
duration.add(lookbackValue, lookbackUnit);
const from = `now-${duration.asSeconds()}s`;
return { invocationCount, interval, from };
};

View file

@ -6,13 +6,7 @@
*/
import { useEffect, useMemo, useState } from 'react';
import moment from 'moment';
import {
RULE_PREVIEW_FROM,
RULE_PREVIEW_INTERVAL,
RULE_PREVIEW_INVOCATION_COUNT,
} from '../../../../../common/detection_engine/constants';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import type {
PreviewResponse,
@ -22,13 +16,8 @@ import type {
import { previewRule } from './api';
import * as i18n from './translations';
import { transformOutput } from './transforms';
import type {
AdvancedPreviewOptions,
QuickQueryPreviewOptions,
} from '../../../pages/detection_engine/rules/types';
import { getTimeTypeValue } from '../../../pages/detection_engine/rules/create/helpers';
const REASONABLE_INVOCATION_COUNT = 200;
import type { TimeframePreviewOptions } from '../../../pages/detection_engine/rules/types';
import { usePreviewInvocationCount } from './use_preview_invocation_count';
const emptyPreviewRule: PreviewResponse = {
previewId: undefined,
@ -37,65 +26,21 @@ const emptyPreviewRule: PreviewResponse = {
};
export const usePreviewRule = ({
quickQueryOptions,
advancedOptions,
timeframeOptions,
}: {
quickQueryOptions: QuickQueryPreviewOptions;
advancedOptions?: AdvancedPreviewOptions;
timeframeOptions: TimeframePreviewOptions;
}) => {
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: string = RULE_PREVIEW_INTERVAL.HOUR;
let from: string = RULE_PREVIEW_FROM.HOUR;
const { invocationCount, interval, from } = usePreviewInvocationCount({ timeframeOptions });
switch (quickQueryOptions.timeframe) {
case 'd':
invocationCount = RULE_PREVIEW_INVOCATION_COUNT.DAY;
interval = RULE_PREVIEW_INTERVAL.DAY;
from = RULE_PREVIEW_FROM.DAY;
break;
case 'w':
invocationCount = RULE_PREVIEW_INVOCATION_COUNT.WEEK;
interval = RULE_PREVIEW_INTERVAL.WEEK;
from = RULE_PREVIEW_FROM.WEEK;
break;
case 'M':
invocationCount = RULE_PREVIEW_INVOCATION_COUNT.MONTH;
interval = RULE_PREVIEW_INTERVAL.MONTH;
from = RULE_PREVIEW_FROM.MONTH;
break;
}
const timeframeEnd = useMemo(
() =>
advancedOptions
? advancedOptions.timeframeEnd.toISOString()
: quickQueryOptions.timeframeEnd.toISOString(),
[advancedOptions, quickQueryOptions]
() => timeframeOptions.timeframeEnd.toISOString(),
[timeframeOptions]
);
if (advancedOptions) {
const timeframeDuration =
(advancedOptions.timeframeEnd.valueOf() / 1000 -
advancedOptions.timeframeStart.valueOf() / 1000) *
1000;
const { unit: intervalUnit, value: intervalValue } = getTimeTypeValue(advancedOptions.interval);
const duration = moment.duration(intervalValue, intervalUnit);
const ruleIntervalDuration = duration.asMilliseconds();
invocationCount = Math.max(Math.ceil(timeframeDuration / ruleIntervalDuration), 1);
interval = advancedOptions.interval;
const { unit: lookbackUnit, value: lookbackValue } = getTimeTypeValue(advancedOptions.lookback);
duration.add(lookbackValue, lookbackUnit);
from = `now-${duration.asSeconds()}s`;
}
const showInvocationCountWarning = invocationCount > REASONABLE_INVOCATION_COUNT;
useEffect(() => {
if (!rule) {
setResponse(emptyPreviewRule);
@ -144,5 +89,5 @@ export const usePreviewRule = ({
};
}, [rule, addError, invocationCount, from, interval, timeframeEnd]);
return { isLoading, showInvocationCountWarning, response, rule, setRule };
return { isLoading, response, rule, setRule };
};

View file

@ -17,7 +17,6 @@ import type {
List,
} from '@kbn/securitysolution-io-ts-list-types';
import type {
ThreatMapping,
Threats,
ThreatSubtechnique,
ThreatTechnique,
@ -27,7 +26,6 @@ import { ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants';
import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../../common/constants';
import { assertUnreachable } from '../../../../../../common/utility_types';
import { transformAlertToRuleAction } from '../../../../../../common/detection_engine/transform_actions';
import type { Rule } from '../../../../containers/detection_engine/rules';
import type {
AboutStepRule,
DefineStepRule,
@ -39,16 +37,10 @@ import type {
ActionsStepRuleJson,
RuleStepsFormData,
RuleStep,
AdvancedPreviewOptions,
} from '../types';
import { DataSourceType } from '../types';
import type { FieldValueQueryBar } from '../../../../components/rules/query_bar';
import type { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request';
import { stepDefineDefaultValue } from '../../../../components/rules/step_define_rule';
import { stepAboutDefaultValue } from '../../../../components/rules/step_about_rule/default_value';
import { stepActionsDefaultValue } from '../../../../components/rules/step_rule_actions';
import type { FieldValueThreshold } from '../../../../components/rules/threshold_input';
import type { EqlOptionsSelected } from '../../../../../../common/search_strategy';
export const getTimeTypeValue = (time: string): { unit: Unit; value: number } => {
const timeObj: { unit: Unit; value: number } = {
@ -568,89 +560,38 @@ export const formatRule = <T>(
aboutStepData: AboutStepRule,
scheduleData: ScheduleStepRule,
actionsData: ActionsStepRule,
rule?: Rule | null
exceptionsList?: List[]
): T =>
deepmerge.all([
formatDefineStepData(defineStepData),
formatAboutStepData(aboutStepData, rule?.exceptions_list),
formatAboutStepData(aboutStepData, exceptionsList),
formatScheduleStepData(scheduleData),
formatActionsStepData(actionsData),
]) as unknown as T;
export const formatPreviewRule = ({
index,
dataViewId,
dataSourceType,
query,
threatIndex,
threatQuery,
ruleType,
threatMapping,
timeFrame,
threshold,
machineLearningJobId,
anomalyThreshold,
eqlOptions,
newTermsFields,
historyWindowSize,
advancedOptions,
defineRuleData,
aboutRuleData,
scheduleRuleData,
exceptionsList,
}: {
index: string[];
dataViewId?: string;
dataSourceType: DataSourceType;
threatIndex: string[];
query: FieldValueQueryBar;
threatQuery: FieldValueQueryBar;
ruleType: Type;
threatMapping: ThreatMapping;
timeFrame: Unit;
threshold: FieldValueThreshold;
machineLearningJobId: string[];
anomalyThreshold: number;
eqlOptions: EqlOptionsSelected;
newTermsFields: string[];
historyWindowSize: string;
advancedOptions?: AdvancedPreviewOptions;
defineRuleData: DefineStepRule;
aboutRuleData: AboutStepRule;
scheduleRuleData: ScheduleStepRule;
exceptionsList?: List[];
}): CreateRulesSchema => {
const defineStepData = {
...stepDefineDefaultValue,
index,
dataViewId,
dataSourceType,
queryBar: query,
ruleType,
threatIndex,
threatQueryBar: threatQuery,
threatMapping,
threshold,
machineLearningJobId,
anomalyThreshold,
eqlOptions,
newTermsFields,
historyWindowSize,
};
const aboutStepData = {
...stepAboutDefaultValue,
...aboutRuleData,
name: 'Preview Rule',
description: 'Preview Rule',
};
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,
defineRuleData,
aboutStepData,
scheduleStepData,
stepActionsDefaultValue
scheduleRuleData,
stepActionsDefaultValue,
exceptionsList
),
...(!advancedOptions ? scheduleStepData : {}),
};
};

View file

@ -6,6 +6,7 @@
*/
import {
EuiButton,
EuiButtonEmpty,
EuiAccordion,
EuiHorizontalRule,
@ -17,6 +18,7 @@ import React, { useCallback, useRef, useState, useMemo, useEffect } from 'react'
import styled from 'styled-components';
import type { DataViewListItem } from '@kbn/data-views-plugin/common';
import { isThreatMatchRule } from '../../../../../../common/detection_engine/utils';
import { useCreateRule } from '../../../../containers/detection_engine/rules';
import type { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request';
import { useListsConfig } from '../../../../containers/detection_engine/lists/use_lists_config';
@ -42,15 +44,33 @@ import {
userHasPermissions,
MaxWidthEuiFlexItem,
} from '../helpers';
import type { RuleStepsFormData, RuleStepsFormHooks } from '../types';
import type {
AboutStepRule,
DefineStepRule,
ScheduleStepRule,
RuleStepsFormData,
RuleStepsFormHooks,
RuleStepsData,
} from '../types';
import { RuleStep } from '../types';
import { formatRule, stepIsValid } from './helpers';
import * as i18n from './translations';
import { SecurityPageName } from '../../../../../app/types';
import { ruleStepsOrder } from '../utils';
import { APP_UI_ID } from '../../../../../../common/constants';
import { useKibana } from '../../../../../common/lib/kibana';
import {
getStepScheduleDefaultValue,
ruleStepsOrder,
stepAboutDefaultValue,
stepDefineDefaultValue,
} from '../utils';
import {
APP_UI_ID,
DEFAULT_INDEX_KEY,
DEFAULT_INDICATOR_SOURCE_PATH,
DEFAULT_THREAT_INDEX_KEY,
} from '../../../../../../common/constants';
import { useKibana, useUiSetting$ } from '../../../../../common/lib/kibana';
import { HeaderPage } from '../../../../../common/components/header_page';
import { PreviewFlyout } from '../preview';
const formHookNoop = async (): Promise<undefined> => undefined;
@ -109,6 +129,10 @@ const CreateRulePageComponent: React.FC = () => {
const scheduleRuleRef = useRef<EuiAccordion | null>(null);
// @ts-expect-error EUI team to resolve: https://github.com/elastic/eui/issues/5985
const ruleActionsRef = useRef<EuiAccordion | null>(null);
const [indicesConfig] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY);
const [threatIndicesConfig] = useUiSetting$<string[]>(DEFAULT_THREAT_INDEX_KEY);
const formHooks = useRef<RuleStepsFormHooks>({
[RuleStep.defineRule]: formHookNoop,
[RuleStep.aboutRule]: formHookNoop,
@ -144,6 +168,44 @@ const CreateRulePageComponent: React.FC = () => {
const ruleName = stepsData.current[RuleStep.aboutRule].data?.name;
const actionMessageParams = useMemo(() => getActionMessageParams(ruleType), [ruleType]);
const [dataViewOptions, setDataViewOptions] = useState<{ [x: string]: DataViewListItem }>({});
const [isPreviewDisabled, setIsPreviewDisabled] = useState(false);
const [isRulePreviewVisible, setIsRulePreviewVisible] = useState(false);
const [defineRuleData, setDefineRuleData] = useState<DefineStepRule>({
...stepDefineDefaultValue,
index: indicesConfig,
threatIndex: threatIndicesConfig,
});
const [aboutRuleData, setAboutRuleData] = useState<AboutStepRule>(stepAboutDefaultValue);
const [scheduleRuleData, setScheduleRuleData] = useState<ScheduleStepRule>(
getStepScheduleDefaultValue(defineRuleData.ruleType)
);
useEffect(() => {
const isThreatMatchRuleValue = isThreatMatchRule(defineRuleData.ruleType);
if (isThreatMatchRuleValue) {
setAboutRuleData({
...stepAboutDefaultValue,
threatIndicatorPath: DEFAULT_INDICATOR_SOURCE_PATH,
});
} else {
setAboutRuleData(stepAboutDefaultValue);
}
setScheduleRuleData(getStepScheduleDefaultValue(defineRuleData.ruleType));
}, [defineRuleData.ruleType]);
const updateCurrentDataState = useCallback(
<K extends keyof RuleStepsData>(data: RuleStepsData[K]) => {
if (activeStep === RuleStep.defineRule) {
setDefineRuleData(data as DefineStepRule);
} else if (activeStep === RuleStep.aboutRule) {
setAboutRuleData(data as AboutStepRule);
} else if (activeStep === RuleStep.scheduleRule) {
setScheduleRuleData(data as ScheduleStepRule);
}
},
[activeStep]
);
useEffect(() => {
const fetchDataViews = async () => {
@ -205,7 +267,8 @@ const CreateRulePageComponent: React.FC = () => {
async (step: RuleStep) => {
const stepData = await formHooks.current[step]();
if (stepData?.isValid) {
if (stepData?.isValid && stepData.data) {
updateCurrentDataState(stepData.data);
setStepData(step, stepData);
const nextStep = getNextStep(step);
@ -235,7 +298,7 @@ const CreateRulePageComponent: React.FC = () => {
}
}
},
[goToStep, setRule]
[goToStep, setRule, updateCurrentDataState]
);
const getAccordionType = useCallback(
@ -250,10 +313,6 @@ const CreateRulePageComponent: React.FC = () => {
[activeStep]
);
const submitStepDefineRule = useCallback(() => {
submitStep(RuleStep.defineRule);
}, [submitStep]);
const defineRuleButton = (
<AccordionTitle
name="1"
@ -326,7 +385,15 @@ const CreateRulePageComponent: React.FC = () => {
}}
isLoading={isLoading || loading}
title={i18n.PAGE_TITLE}
/>
>
<EuiButton
data-test-subj="preview-flyout"
iconType="visBarVerticalStacked"
onClick={() => setIsRulePreviewVisible((isVisible) => !isVisible)}
>
{i18n.RULE_PREVIEW_TITLE}
</EuiButton>
</HeaderPage>
<MyEuiPanel zindex={4} hasBorder>
<EuiAccordion
initialIsOpen={true}
@ -351,16 +418,20 @@ const CreateRulePageComponent: React.FC = () => {
<EuiHorizontalRule margin="m" />
<StepDefineRule
addPadding={true}
defaultValues={stepsData.current[RuleStep.defineRule].data}
defaultValues={defineRuleData}
isReadOnlyView={activeStep !== RuleStep.defineRule}
isLoading={isLoading || loading}
setForm={setFormHook}
onSubmit={submitStepDefineRule}
onSubmit={() => submitStep(RuleStep.defineRule)}
kibanaDataViews={dataViewOptions}
descriptionColumns="singleSplit"
// We need a key to make this component remount when edit/view mode is toggled
// https://github.com/elastic/kibana/pull/132834#discussion_r881705566
key={isShouldRerenderStep(RuleStep.defineRule, activeStep)}
indicesConfig={indicesConfig}
threatIndicesConfig={threatIndicesConfig}
onRuleDataChange={updateCurrentDataState}
onPreviewDisabledStateChange={setIsPreviewDisabled}
/>
</EuiAccordion>
</MyEuiPanel>
@ -389,8 +460,8 @@ const CreateRulePageComponent: React.FC = () => {
<EuiHorizontalRule margin="m" />
<StepAboutRule
addPadding={true}
defaultValues={stepsData.current[RuleStep.aboutRule].data}
defineRuleData={stepsData.current[RuleStep.defineRule].data}
defaultValues={aboutRuleData}
defineRuleData={defineRuleData}
descriptionColumns="singleSplit"
isReadOnlyView={activeStep !== RuleStep.aboutRule}
isLoading={isLoading || loading}
@ -399,6 +470,7 @@ const CreateRulePageComponent: React.FC = () => {
// We need a key to make this component remount when edit/view mode is toggled
// https://github.com/elastic/kibana/pull/132834#discussion_r881705566
key={isShouldRerenderStep(RuleStep.aboutRule, activeStep)}
onRuleDataChange={updateCurrentDataState}
/>
</EuiAccordion>
</MyEuiPanel>
@ -425,9 +497,8 @@ const CreateRulePageComponent: React.FC = () => {
>
<EuiHorizontalRule margin="m" />
<StepScheduleRule
ruleType={ruleType}
addPadding={true}
defaultValues={stepsData.current[RuleStep.scheduleRule].data}
defaultValues={scheduleRuleData}
descriptionColumns="singleSplit"
isReadOnlyView={activeStep !== RuleStep.scheduleRule}
isLoading={isLoading || loading}
@ -436,6 +507,7 @@ const CreateRulePageComponent: React.FC = () => {
// We need a key to make this component remount when edit/view mode is toggled
// https://github.com/elastic/kibana/pull/132834#discussion_r881705566
key={isShouldRerenderStep(RuleStep.scheduleRule, activeStep)}
onRuleDataChange={updateCurrentDataState}
/>
</EuiAccordion>
</MyEuiPanel>
@ -475,6 +547,15 @@ const CreateRulePageComponent: React.FC = () => {
/>
</EuiAccordion>
</MyEuiPanel>
{isRulePreviewVisible && (
<PreviewFlyout
isDisabled={isPreviewDisabled && activeStep === RuleStep.defineRule}
defineStepData={defineRuleData}
aboutStepData={aboutRuleData}
scheduleStepData={scheduleRuleData}
onClose={() => setIsRulePreviewVisible(false)}
/>
)}
</MaxWidthEuiFlexItem>
</EuiFlexGroup>
</SecuritySolutionPageWrapper>

View file

@ -21,6 +21,28 @@ export const BACK_TO_RULES = i18n.translate(
}
);
export const RULE_PREVIEW_TITLE = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.rulePreviewTitle',
{
defaultMessage: 'Rule preview',
}
);
export const RULE_PREVIEW_DESCRIPTION = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.rulePreviewDescription',
{
defaultMessage:
'Rule preview reflects the current configuration of your rule settings and exceptions, click refresh icon to see the updated preview.',
}
);
export const CANCEL_BUTTON_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.cancelButtonLabel',
{
defaultMessage: 'Cancel',
}
);
export const EDIT_RULE = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.editRuleButton',
{

View file

@ -37,7 +37,7 @@ import {
useDeepEqualSelector,
useShallowEqualSelector,
} from '../../../../../common/hooks/use_selector';
import { useKibana } from '../../../../../common/lib/kibana';
import { useKibana, useUiSetting$ } from '../../../../../common/lib/kibana';
import { TimelineId } from '../../../../../../common/types/timeline';
import type { UpdateDateRange } from '../../../../../common/components/charts/common';
import { FiltersGlobal } from '../../../../../common/components/filters_global';
@ -78,7 +78,11 @@ import { hasMlLicense } from '../../../../../../common/machine_learning/has_ml_l
import { SecurityPageName } from '../../../../../app/types';
import { LinkButton } from '../../../../../common/components/links';
import { useFormatUrl } from '../../../../../common/components/link_to';
import { APP_UI_ID } from '../../../../../../common/constants';
import {
APP_UI_ID,
DEFAULT_INDEX_KEY,
DEFAULT_THREAT_INDEX_KEY,
} from '../../../../../../common/constants';
import { useGlobalFullScreen } from '../../../../../common/containers/use_full_screen';
import { Display } from '../../../../../hosts/pages/display';
@ -301,6 +305,9 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
const [filterGroup, setFilterGroup] = useState<Status>(FILTER_OPEN);
const [dataViewOptions, setDataViewOptions] = useState<{ [x: string]: DataViewListItem }>({});
const [indicesConfig] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY);
const [threatIndicesConfig] = useUiSetting$<string[]>(DEFAULT_THREAT_INDEX_KEY);
useEffect(() => {
const fetchDataViews = async () => {
const dataViewsRefs = await data.dataViews.getIdsWithTitle();
@ -767,6 +774,8 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
isLoading={false}
defaultValues={{ dataViewTitle, ...defineRuleData }}
kibanaDataViews={dataViewOptions}
indicesConfig={indicesConfig}
threatIndicesConfig={threatIndicesConfig}
/>
)}
</StepPanel>

View file

@ -52,16 +52,29 @@ import {
MaxWidthEuiFlexItem,
} from '../helpers';
import * as ruleI18n from '../translations';
import type { RuleStepsFormHooks, RuleStepsFormData, RuleStepsData } from '../types';
import type {
ActionsStepRule,
AboutStepRule,
DefineStepRule,
ScheduleStepRule,
RuleStepsFormHooks,
RuleStepsFormData,
RuleStepsData,
} from '../types';
import { RuleStep } from '../types';
import * as i18n from './translations';
import { SecurityPageName } from '../../../../../app/types';
import { ruleStepsOrder } from '../utils';
import { useKibana } from '../../../../../common/lib/kibana';
import { APP_UI_ID } from '../../../../../../common/constants';
import { useKibana, useUiSetting$ } from '../../../../../common/lib/kibana';
import {
APP_UI_ID,
DEFAULT_INDEX_KEY,
DEFAULT_THREAT_INDEX_KEY,
} from '../../../../../../common/constants';
import { HeaderPage } from '../../../../../common/components/header_page';
import { useStartTransaction } from '../../../../../common/lib/apm/use_start_transaction';
import { SINGLE_RULE_ACTIONS } from '../../../../../common/lib/apm/user_actions';
import { PreviewFlyout } from '../preview';
const formHookNoop = async (): Promise<undefined> => undefined;
@ -97,10 +110,10 @@ const EditRulePageComponent: FC = () => {
[RuleStep.scheduleRule]: { isValid: false, data: undefined },
[RuleStep.ruleActions]: { isValid: false, data: undefined },
});
const defineStep = stepsData.current[RuleStep.defineRule];
const aboutStep = stepsData.current[RuleStep.aboutRule];
const scheduleStep = stepsData.current[RuleStep.scheduleRule];
const actionsStep = stepsData.current[RuleStep.ruleActions];
const [defineStep, setDefineStep] = useState(stepsData.current[RuleStep.defineRule]);
const [aboutStep, setAboutStep] = useState(stepsData.current[RuleStep.aboutRule]);
const [scheduleStep, setScheduleStep] = useState(stepsData.current[RuleStep.scheduleRule]);
const [actionsStep, setActionsStep] = useState(stepsData.current[RuleStep.ruleActions]);
const [activeStep, setActiveStep] = useState<RuleStep>(RuleStep.defineRule);
const invalidSteps = ruleStepsOrder.filter((step) => {
const stepData = stepsData.current[step];
@ -108,6 +121,8 @@ const EditRulePageComponent: FC = () => {
});
const [{ isLoading, isSaved }, setRule] = useUpdateRule();
const [dataViewOptions, setDataViewOptions] = useState<{ [x: string]: DataViewListItem }>({});
const [isPreviewDisabled, setIsPreviewDisabled] = useState(false);
const [isRulePreviewVisible, setIsRulePreviewVisible] = useState(false);
useEffect(() => {
const fetchDataViews = async () => {
@ -135,11 +150,51 @@ const EditRulePageComponent: FC = () => {
);
const setStepData = useCallback(
<K extends keyof RuleStepsData>(step: K, data: RuleStepsData[K], isValid: boolean) => {
stepsData.current[step] = { ...stepsData.current[step], data, isValid };
switch (step) {
case RuleStep.aboutRule:
const aboutData = data as AboutStepRule;
setAboutStep({ ...stepsData.current[step], data: aboutData, isValid });
return;
case RuleStep.defineRule:
const defineData = data as DefineStepRule;
setDefineStep({ ...stepsData.current[step], data: defineData, isValid });
return;
case RuleStep.ruleActions:
const actionsData = data as ActionsStepRule;
setActionsStep({ ...stepsData.current[step], data: actionsData, isValid });
return;
case RuleStep.scheduleRule:
const scheduleData = data as ScheduleStepRule;
setScheduleStep({ ...stepsData.current[step], data: scheduleData, isValid });
}
},
[]
);
const onDataChange = useCallback(async () => {
if (activeStep === RuleStep.defineRule) {
const defineStepData = await formHooks.current[RuleStep.defineRule]();
if (defineStepData?.isValid && defineStepData?.data) {
setDefineStep(defineStepData);
}
} else if (activeStep === RuleStep.aboutRule) {
const aboutStepData = await formHooks.current[RuleStep.aboutRule]();
if (aboutStepData?.isValid && aboutStepData?.data) {
setAboutStep(aboutStepData);
}
} else if (activeStep === RuleStep.scheduleRule) {
const scheduleStepData = await formHooks.current[RuleStep.scheduleRule]();
if (scheduleStepData?.isValid && scheduleStepData?.data) {
setScheduleStep(scheduleStepData);
}
}
}, [activeStep]);
const onPreviewClose = useCallback(() => setIsRulePreviewVisible(false), []);
const [indicesConfig] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY);
const [threatIndicesConfig] = useUiSetting$<string[]>(DEFAULT_THREAT_INDEX_KEY);
const tabs = useMemo(
() => [
{
@ -159,6 +214,10 @@ const EditRulePageComponent: FC = () => {
defaultValues={defineStep.data}
setForm={setFormHook}
kibanaDataViews={dataViewOptions}
indicesConfig={indicesConfig}
threatIndicesConfig={threatIndicesConfig}
onRuleDataChange={onDataChange}
onPreviewDisabledStateChange={setIsPreviewDisabled}
/>
)}
<EuiSpacer />
@ -183,6 +242,7 @@ const EditRulePageComponent: FC = () => {
defaultValues={aboutStep.data}
defineRuleData={defineStep.data}
setForm={setFormHook}
onRuleDataChange={onDataChange}
/>
)}
<EuiSpacer />
@ -206,6 +266,7 @@ const EditRulePageComponent: FC = () => {
isUpdateView
defaultValues={scheduleStep.data}
setForm={setFormHook}
onRuleDataChange={onDataChange}
/>
)}
<EuiSpacer />
@ -243,11 +304,14 @@ const EditRulePageComponent: FC = () => {
defineStep.data,
isLoading,
setFormHook,
dataViewOptions,
indicesConfig,
threatIndicesConfig,
onDataChange,
aboutStep.data,
scheduleStep.data,
actionsStep.data,
actionMessageParams,
dataViewOptions,
]
);
@ -276,7 +340,7 @@ const EditRulePageComponent: FC = () => {
about.data,
schedule.data,
actions.data,
rule
rule?.exceptions_list
),
...(ruleId ? { id: ruleId } : {}),
...(rule != null ? { max_signals: rule.max_signals } : {}),
@ -388,7 +452,16 @@ const EditRulePageComponent: FC = () => {
}}
isLoading={isLoading}
title={i18n.PAGE_TITLE}
/>
>
{defineStep.data && aboutStep.data && scheduleStep.data && (
<EuiButton
iconType="visBarVerticalStacked"
onClick={() => setIsRulePreviewVisible((isVisible) => !isVisible)}
>
{ruleI18n.RULE_PREVIEW_TITLE}
</EuiButton>
)}
</HeaderPage>
{invalidSteps.length > 0 && (
<EuiCallOut title={i18n.SORRY_ERRORS} color="danger" iconType="alert">
<FormattedMessage
@ -449,6 +522,16 @@ const EditRulePageComponent: FC = () => {
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
{isRulePreviewVisible && defineStep.data && aboutStep.data && scheduleStep.data && (
<PreviewFlyout
isDisabled={isPreviewDisabled}
defineStepData={defineStep.data}
aboutStepData={aboutStep.data}
scheduleStepData={scheduleStep.data}
exceptionsList={rule?.exceptions_list}
onClose={onPreviewClose}
/>
)}
</MaxWidthEuiFlexItem>
</EuiFlexGroup>
</SecuritySolutionPageWrapper>

View file

@ -0,0 +1,80 @@
/*
* 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.
*/
import {
EuiButton,
EuiSpacer,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiText,
EuiTitle,
} from '@elastic/eui';
import React from 'react';
import styled from 'styled-components';
import type { List } from '@kbn/securitysolution-io-ts-list-types';
import { RulePreview } from '../../../../components/rules/rule_preview';
import type { AboutStepRule, DefineStepRule, ScheduleStepRule } from '../types';
import * as i18n from './translations';
const StyledEuiFlyoutBody = styled(EuiFlyoutBody)`
overflow-y: hidden;
flex: 1;
.euiFlyoutBody__overflow {
mask-image: none;
}
`;
interface PreviewFlyoutProps {
isDisabled: boolean;
defineStepData: DefineStepRule;
aboutStepData: AboutStepRule;
scheduleStepData: ScheduleStepRule;
exceptionsList?: List[];
onClose: () => void;
}
const PreviewFlyoutComponent: React.FC<PreviewFlyoutProps> = ({
isDisabled,
defineStepData,
aboutStepData,
scheduleStepData,
exceptionsList,
onClose,
}) => {
return (
<EuiFlyout type="push" size="550px" ownFocus={false} onClose={onClose}>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2>{i18n.RULE_PREVIEW_TITLE}</h2>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText color="subdued">
<p>{i18n.RULE_PREVIEW_DESCRIPTION}</p>
</EuiText>
</EuiFlyoutHeader>
<StyledEuiFlyoutBody>
<RulePreview
isDisabled={isDisabled}
defineRuleData={defineStepData}
aboutRuleData={aboutStepData}
scheduleRuleData={scheduleStepData}
exceptionsList={exceptionsList}
/>
</StyledEuiFlyoutBody>
<EuiFlyoutFooter>
<EuiButton onClick={onClose}>{i18n.CANCEL_BUTTON_LABEL}</EuiButton>
</EuiFlyoutFooter>
</EuiFlyout>
);
};
export const PreviewFlyout = React.memo(PreviewFlyoutComponent);

View file

@ -0,0 +1,30 @@
/*
* 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.
*/
import { i18n } from '@kbn/i18n';
export const RULE_PREVIEW_TITLE = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.rulePreviewTitle',
{
defaultMessage: 'Rule preview',
}
);
export const RULE_PREVIEW_DESCRIPTION = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.rulePreviewDescription',
{
defaultMessage:
'Rule preview reflects the current configuration of your rule settings and exceptions, click refresh icon to see the updated preview.',
}
);
export const CANCEL_BUTTON_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.cancelButtonLabel',
{
defaultMessage: 'Cancel',
}
);

View file

@ -1090,3 +1090,25 @@ export const NEW_TERMS_TOUR_CONTENT = i18n.translate(
defaultMessage: '"New Terms" rules alert on values that have not previously been seen',
}
);
export const RULE_PREVIEW_TITLE = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.rulePreviewTitle',
{
defaultMessage: 'Rule preview',
}
);
export const RULE_PREVIEW_DESCRIPTION = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.rulePreviewDescription',
{
defaultMessage:
'Rule preview reflects the current configuration of your rule settings and exceptions, click refresh icon to see the updated preview.',
}
);
export const CANCEL_BUTTON_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.cancelButtonLabel',
{
defaultMessage: 'Cancel',
}
);

View file

@ -16,10 +16,9 @@ import type {
SeverityMapping,
Severity,
} from '@kbn/securitysolution-io-ts-alerting-types';
import type { Filter } from '@kbn/es-query';
import type { DataViewBase, Filter } from '@kbn/es-query';
import type { RuleAction } from '@kbn/alerting-plugin/common';
import type { DataViewListItem } from '@kbn/data-views-plugin/common';
import type { Unit } from '@kbn/datemath';
import type { RuleAlertAction } from '../../../../../common/detection_engine/types';
import type { FieldValueQueryBar } from '../../../components/rules/query_bar';
@ -146,6 +145,7 @@ export enum DataSourceType {
export interface DefineStepRule {
anomalyThreshold: number;
index: string[];
indexPattern?: DataViewBase;
machineLearningJobId: string[];
queryBar: FieldValueQueryBar;
dataViewId?: string;
@ -243,17 +243,7 @@ export interface ActionsStepRuleJson {
meta?: unknown;
}
export interface QuickQueryPreviewOptions {
timeframe: Unit;
timeframeEnd: moment.Moment;
}
export interface AdvancedPreviewForm {
interval: string;
lookback: string;
}
export interface AdvancedPreviewOptions {
export interface TimeframePreviewOptions {
timeframeStart: moment.Moment;
timeframeEnd: moment.Moment;
interval: string;

View file

@ -6,6 +6,9 @@
*/
import type { ChromeBreadcrumb } from '@kbn/core/public';
import type { Type } from '@kbn/securitysolution-io-ts-alerting-types';
import { isThreatMatchRule } from '../../../../../common/detection_engine/utils';
import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations';
import {
getRuleDetailsTabUrl,
getRuleDetailsUrl,
@ -13,11 +16,12 @@ import {
import * as i18nRules from './translations';
import type { RouteSpyState } from '../../../../common/utils/route/types';
import { SecurityPageName } from '../../../../app/types';
import { RULES_PATH } from '../../../../../common/constants';
import type { RuleStepsOrder } from './types';
import { RuleStep } from './types';
import { DEFAULT_THREAT_MATCH_QUERY, RULES_PATH } from '../../../../../common/constants';
import type { AboutStepRule, DefineStepRule, RuleStepsOrder, ScheduleStepRule } from './types';
import { DataSourceType, RuleStep } from './types';
import type { GetSecuritySolutionUrl } from '../../../../common/components/link_to';
import { RuleDetailTabs, RULE_DETAILS_TAB_NAME } from './details';
import { fillEmptySeverityMappings } from './helpers';
export const ruleStepsOrder: RuleStepsOrder = [
RuleStep.defineRule,
@ -90,3 +94,97 @@ export const getTrailingBreadcrumbs = (
return breadcrumb;
};
export const threatDefault = [
{
framework: 'MITRE ATT&CK',
tactic: { id: 'none', name: 'none', reference: 'none' },
technique: [],
},
];
export const stepDefineDefaultValue: DefineStepRule = {
anomalyThreshold: 50,
index: [],
indexPattern: { fields: [], title: '' },
machineLearningJobId: [],
ruleType: 'query',
threatIndex: [],
queryBar: {
query: { query: '', language: 'kuery' },
filters: [],
saved_id: null,
},
threatQueryBar: {
query: { query: DEFAULT_THREAT_MATCH_QUERY, language: 'kuery' },
filters: [],
saved_id: null,
},
requiredFields: [],
relatedIntegrations: [],
threatMapping: [],
threshold: {
field: [],
value: '200',
cardinality: {
field: [],
value: '',
},
},
timeline: {
id: null,
title: DEFAULT_TIMELINE_TITLE,
},
eqlOptions: {},
dataSourceType: DataSourceType.IndexPatterns,
newTermsFields: [],
historyWindowSize: '7d',
};
export const stepAboutDefaultValue: AboutStepRule = {
author: [],
name: '',
description: '',
isAssociatedToEndpointList: false,
isBuildingBlock: false,
severity: { value: 'low', mapping: fillEmptySeverityMappings([]), isMappingChecked: false },
riskScore: { value: 21, mapping: [], isMappingChecked: false },
references: [''],
falsePositives: [''],
license: '',
ruleNameOverride: '',
tags: [],
timestampOverride: '',
threat: threatDefault,
note: '',
threatIndicatorPath: undefined,
timestampOverrideFallbackDisabled: undefined,
};
const DEFAULT_INTERVAL = '5m';
const DEFAULT_FROM = '1m';
const THREAT_MATCH_INTERVAL = '1h';
const THREAT_MATCH_FROM = '5m';
export const getStepScheduleDefaultValue = (ruleType: Type | undefined): ScheduleStepRule => {
return {
interval: isThreatMatchRule(ruleType) ? THREAT_MATCH_INTERVAL : DEFAULT_INTERVAL,
from: isThreatMatchRule(ruleType) ? THREAT_MATCH_FROM : DEFAULT_FROM,
};
};
/**
* This default query will be used for threat query/indicator matches
* as the default when the user swaps to using it by changing their
* rule type from any rule type to the "threatMatchRule" type. Only
* difference is that "*:*" is used instead of '' for its query.
*/
const threatQueryBarDefaultValue: DefineStepRule['queryBar'] = {
...stepDefineDefaultValue.queryBar,
query: { ...stepDefineDefaultValue.queryBar.query, query: '*:*' },
};
export const defaultCustomQuery = {
forNormalRules: stepDefineDefaultValue.queryBar,
forThreatMatchRules: threatQueryBarDefaultValue,
};

View file

@ -71,7 +71,7 @@ export const convertToBuildEsQuery = ({
filters,
}: {
config: EsQueryConfig;
indexPattern: DataViewBase;
indexPattern: DataViewBase | undefined;
queries: Query[];
filters: Filter[];
}): [string, undefined] | [undefined, Error] => {

View file

@ -27087,18 +27087,12 @@
"xpack.securitySolution.detectionEngine.noPermissionsMessage": "Pour afficher les alertes, vous devez mettre à jour les privilèges. Pour en savoir plus, contactez votre administrateur Kibana.",
"xpack.securitySolution.detectionEngine.noPermissionsTitle": "Privilèges requis",
"xpack.securitySolution.detectionEngine.pageTitle": "Moteur de détection",
"xpack.securitySolution.detectionEngine.previewRule.fieldAdditionalLookBackHelpText": "Ajoute du temps à la période de récupération pour éviter de manquer des alertes.",
"xpack.securitySolution.detectionEngine.previewRule.fieldAdditionalLookBackLabel": "Temps de récupération supplémentaire",
"xpack.securitySolution.detectionEngine.previewRule.fieldIntervalHelpText": "Les règles s'exécutent de façon régulière et détectent les alertes dans la période de temps spécifiée.",
"xpack.securitySolution.detectionEngine.previewRule.fieldIntervalLabel": "S'exécute toutes les (intervalle des règles)",
"xpack.securitySolution.detectionEngine.queryPreview.actions": "Actions",
"xpack.securitySolution.detectionEngine.queryPreview.histogramDisclaimer": "Remarque : Les alertes ayant plusieurs valeurs event.category seront comptées plusieurs fois.",
"xpack.securitySolution.detectionEngine.queryPreview.mlHistogramDisclaimer": "Remarque : Les alertes ayant plusieurs valeurs host.name seront comptées plusieurs fois.",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphCountLabel": "Décompte",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewError": "Erreur de récupération de l'aperçu",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewNoiseWarning": "Avertissement de bruit : cette règle peut générer beaucoup de bruit. Envisagez d'affiner votre recherche. La base est une progression linéaire comportant 1 alerte par heure.",
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewDisclaimer": "Remarque : cet aperçu exclut les effets d'exceptions aux règles et les remplacements d'horodatages.",
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewHelpText": "Sélectionnez une période de temps pour les données afin d'afficher l'aperçu des résultats de requête.",
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewInvocationCountWarningMessage": "La durée et l'intervalle de règle que vous avez sélectionnés pour la prévisualisation de cette règle peuvent provoquer un dépassement de délai ou prendre beaucoup de temps à s'exécuter. Essayez de réduire la durée et/ou d'augmenter l'intervalle si l'aperçu a expiré (cela n'affectera pas l'exécution de la règle).",
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewInvocationCountWarningTitle": "Le délai d'aperçu des règles peut entraîner un dépassement de délai",
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewLabel": "Durée",

View file

@ -27064,18 +27064,12 @@
"xpack.securitySolution.detectionEngine.noPermissionsMessage": "アラートを表示するには、権限を更新する必要があります。詳細については、Kibana管理者に連絡してください。",
"xpack.securitySolution.detectionEngine.noPermissionsTitle": "権限が必要です",
"xpack.securitySolution.detectionEngine.pageTitle": "検出エンジン",
"xpack.securitySolution.detectionEngine.previewRule.fieldAdditionalLookBackHelpText": "ルックバック期間に時間を追加してアラートの見落としを防ぎます。",
"xpack.securitySolution.detectionEngine.previewRule.fieldAdditionalLookBackLabel": "追加のルックバック時間",
"xpack.securitySolution.detectionEngine.previewRule.fieldIntervalHelpText": "ルールを定期的に実行し、指定の時間枠内でアラートを検出します。",
"xpack.securitySolution.detectionEngine.previewRule.fieldIntervalLabel": "次の間隔で実行(ルール間隔)",
"xpack.securitySolution.detectionEngine.queryPreview.actions": "アクション",
"xpack.securitySolution.detectionEngine.queryPreview.histogramDisclaimer": "注複数のevent.category値のアラートは2回以上カウントされます。",
"xpack.securitySolution.detectionEngine.queryPreview.mlHistogramDisclaimer": "注複数のhost.name値のアラートは2回以上カウントされます。",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphCountLabel": "カウント",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewError": "プレビュー取得エラー",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewNoiseWarning": "イズ警告このルールではイズが多く生じる可能性があります。クエリを絞り込むことを検討してください。これは1時間ごとに1アラートという線形進行に基づいています。",
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewDisclaimer": "注:このプレビューは、ルール例外とタイムスタンプオーバーライドの効果を除外します。",
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewHelpText": "クエリ結果をプレビューするデータのタイムフレームを選択します。",
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewInvocationCountWarningMessage": "このルールのプレビューで選択したタイムフレームとルール間隔は、タイムアウトになるか、実行に時間がかかる可能性があります。プレビューがタイムアウトする場合は、タイムフレームを短くしたり、間隔を長くしたりしてください(これは実際のルール実行には影響しません)。",
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewInvocationCountWarningTitle": "ルールプレビュータイムフレームはタイムアウトが発生する場合があります",
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewLabel": "タイムフレーム",

View file

@ -27095,18 +27095,12 @@
"xpack.securitySolution.detectionEngine.noPermissionsMessage": "要查看告警,必须更新权限。有关详细信息,请联系您的 Kibana 管理员。",
"xpack.securitySolution.detectionEngine.noPermissionsTitle": "需要权限",
"xpack.securitySolution.detectionEngine.pageTitle": "检测引擎",
"xpack.securitySolution.detectionEngine.previewRule.fieldAdditionalLookBackHelpText": "增加回查时段的时间以防止错过告警。",
"xpack.securitySolution.detectionEngine.previewRule.fieldAdditionalLookBackLabel": "更多回查时间",
"xpack.securitySolution.detectionEngine.previewRule.fieldIntervalHelpText": "规则定期运行并检测指定时间范围内的告警。",
"xpack.securitySolution.detectionEngine.previewRule.fieldIntervalLabel": "运行间隔(规则时间间隔)",
"xpack.securitySolution.detectionEngine.queryPreview.actions": "操作",
"xpack.securitySolution.detectionEngine.queryPreview.histogramDisclaimer": "注意:具有多个 event.category 值的告警会计算多次。",
"xpack.securitySolution.detectionEngine.queryPreview.mlHistogramDisclaimer": "注意:具有多个 host.name 值的告警会计算多次。",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphCountLabel": "计数",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewError": "提取预览时出错",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewNoiseWarning": "噪音警告:此规则可能会导致大量噪音。考虑缩小您的查询范围。这基于每小时 1 条告警的线性级数。",
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewDisclaimer": "注意:此预览不包括规则例外和时间戳覆盖的影响。",
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewHelpText": "选择数据的时间范围以预览查询结果。",
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewInvocationCountWarningMessage": "您为预览此规则选择的时间范围和规则时间间隔可能会导致超时或需要很长时间才能完成执行。如果预览超时,请尝试减少时间范围和/或增加时间间隔(这不会影响实际规则运行)。",
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewInvocationCountWarningTitle": "规则预览时间范围可能会导致超时",
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewLabel": "时间范围",