mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
d2844f7cda
commit
cbe7dc8106
35 changed files with 941 additions and 934 deletions
|
@ -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"]';
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.',
|
||||
}
|
||||
),
|
||||
},
|
||||
};
|
|
@ -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',
|
||||
{
|
||||
|
|
|
@ -21,7 +21,7 @@ interface PreviewHistogramParams {
|
|||
startDate: string;
|
||||
spaceId: string;
|
||||
ruleType: Type;
|
||||
indexPattern: DataViewBase;
|
||||
indexPattern: DataViewBase | undefined;
|
||||
}
|
||||
|
||||
export const usePreviewHistogram = ({
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 };
|
||||
};
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
@ -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 : {}),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
{
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -71,7 +71,7 @@ export const convertToBuildEsQuery = ({
|
|||
filters,
|
||||
}: {
|
||||
config: EsQueryConfig;
|
||||
indexPattern: DataViewBase;
|
||||
indexPattern: DataViewBase | undefined;
|
||||
queries: Query[];
|
||||
filters: Filter[];
|
||||
}): [string, undefined] | [undefined, Error] => {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "タイムフレーム",
|
||||
|
|
|
@ -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": "时间范围",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue