mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[9.0] [Security Solution][Detection Engine] fixes rule preview works for form's invalid state (#173930) (#213801) (#214744)
# Backport This will backport the following commits from `main` to `9.0`: - [[Security Solution][Detection Engine] fixes rule preview works for form's invalid state (#173930) (#213801)](https://github.com/elastic/kibana/pull/213801) <!--- Backport version: 9.6.6 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Edgar Santos","email":"edgar.santos@elastic.co"},"sourceCommit":{"committedDate":"2025-03-17T12:56:13Z","message":"[Security Solution][Detection Engine] fixes rule preview works for form's invalid state (#173930) (#213801)\n\n# Summary\nThis PR changes the behavior of the Preview rule button in the create\nrule form. Now the button will always be enabled, but upon clicking it\nthe validation of the define rule form will be triggered. If the form is\ninvalid, it will show a warning redirecting the user to the form in\norder to fix the errors.\n\nThis PR removes the helper function `getIsRulePreviewDisabled` after\nverifying that all the validations that it was doing are already covered\nby the validation rules of the define rule form.\n\n\nThis solves issue #173930\n\n---------\n\nCo-authored-by: Nastasha Solomon <79124755+nastasha-solomon@users.noreply.github.com>","sha":"07baf3a79f5d2e37ba2a4823c3c4ef339684959d","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","v9.0.0","Feature:Detection Rules","Feature:Detection Rule Preview","Team:Detection Engine","backport:version","v8.18.0","v9.1.0","v8.17.4"],"title":"[Security Solution][Detection Engine] fixes rule preview works for form's invalid state (#173930)","number":213801,"url":"https://github.com/elastic/kibana/pull/213801","mergeCommit":{"message":"[Security Solution][Detection Engine] fixes rule preview works for form's invalid state (#173930) (#213801)\n\n# Summary\nThis PR changes the behavior of the Preview rule button in the create\nrule form. Now the button will always be enabled, but upon clicking it\nthe validation of the define rule form will be triggered. If the form is\ninvalid, it will show a warning redirecting the user to the form in\norder to fix the errors.\n\nThis PR removes the helper function `getIsRulePreviewDisabled` after\nverifying that all the validations that it was doing are already covered\nby the validation rules of the define rule form.\n\n\nThis solves issue #173930\n\n---------\n\nCo-authored-by: Nastasha Solomon <79124755+nastasha-solomon@users.noreply.github.com>","sha":"07baf3a79f5d2e37ba2a4823c3c4ef339684959d"}},"sourceBranch":"main","suggestedTargetBranches":["9.0","8.18","8.17"],"targetPullRequestStates":[{"branch":"9.0","label":"v9.0.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.18","label":"v8.18.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/213801","number":213801,"mergeCommit":{"message":"[Security Solution][Detection Engine] fixes rule preview works for form's invalid state (#173930) (#213801)\n\n# Summary\nThis PR changes the behavior of the Preview rule button in the create\nrule form. Now the button will always be enabled, but upon clicking it\nthe validation of the define rule form will be triggered. If the form is\ninvalid, it will show a warning redirecting the user to the form in\norder to fix the errors.\n\nThis PR removes the helper function `getIsRulePreviewDisabled` after\nverifying that all the validations that it was doing are already covered\nby the validation rules of the define rule form.\n\n\nThis solves issue #173930\n\n---------\n\nCo-authored-by: Nastasha Solomon <79124755+nastasha-solomon@users.noreply.github.com>","sha":"07baf3a79f5d2e37ba2a4823c3c4ef339684959d"}},{"branch":"8.17","label":"v8.17.4","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Edgar Santos <edgar.santos@elastic.co>
This commit is contained in:
parent
c8e9793ab3
commit
266bcefdc9
7 changed files with 79 additions and 425 deletions
|
@ -6,8 +6,7 @@
|
|||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
import { DataSourceType } from '../../../../detections/pages/detection_engine/rules/types';
|
||||
import { isNoisy, getTimeframeOptions, getIsRulePreviewDisabled } from './helpers';
|
||||
import { isNoisy, getTimeframeOptions } from './helpers';
|
||||
|
||||
describe('query_preview/helpers', () => {
|
||||
const timeframeEnd = moment();
|
||||
|
@ -84,282 +83,6 @@ describe('query_preview/helpers', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('isRulePreviewDisabled', () => {
|
||||
test('disabled when there is no index', () => {
|
||||
const isDisabled = getIsRulePreviewDisabled({
|
||||
ruleType: 'threat_match',
|
||||
isQueryBarValid: true,
|
||||
isThreatQueryBarValid: true,
|
||||
index: [],
|
||||
dataViewId: undefined,
|
||||
dataSourceType: DataSourceType.IndexPatterns,
|
||||
threatIndex: ['threat-*'],
|
||||
threatMapping: [
|
||||
{ entries: [{ field: 'test-field', value: 'test-value', type: 'mapping' }] },
|
||||
],
|
||||
machineLearningJobId: ['test-ml-job-id'],
|
||||
queryBar: { filters: [], query: { query: '', language: 'testlang' }, saved_id: null },
|
||||
newTermsFields: [],
|
||||
});
|
||||
expect(isDisabled).toEqual(true);
|
||||
});
|
||||
|
||||
test('disabled when query bar is invalid', () => {
|
||||
const isDisabled = getIsRulePreviewDisabled({
|
||||
ruleType: 'threat_match',
|
||||
isQueryBarValid: false,
|
||||
isThreatQueryBarValid: true,
|
||||
index: ['test-*'],
|
||||
dataViewId: undefined,
|
||||
dataSourceType: DataSourceType.IndexPatterns,
|
||||
threatIndex: ['threat-*'],
|
||||
threatMapping: [
|
||||
{ entries: [{ field: 'test-field', value: 'test-value', type: 'mapping' }] },
|
||||
],
|
||||
machineLearningJobId: ['test-ml-job-id'],
|
||||
queryBar: { filters: [], query: { query: '', language: 'testlang' }, saved_id: null },
|
||||
newTermsFields: [],
|
||||
});
|
||||
expect(isDisabled).toEqual(true);
|
||||
});
|
||||
|
||||
test('disabled when threat query bar is invalid', () => {
|
||||
const isDisabled = getIsRulePreviewDisabled({
|
||||
ruleType: 'threat_match',
|
||||
isQueryBarValid: true,
|
||||
isThreatQueryBarValid: false,
|
||||
index: ['test-*'],
|
||||
dataViewId: undefined,
|
||||
dataSourceType: DataSourceType.IndexPatterns,
|
||||
threatIndex: ['threat-*'],
|
||||
threatMapping: [
|
||||
{ entries: [{ field: 'test-field', value: 'test-value', type: 'mapping' }] },
|
||||
],
|
||||
machineLearningJobId: ['test-ml-job-id'],
|
||||
queryBar: { filters: [], query: { query: '', language: 'testlang' }, saved_id: null },
|
||||
newTermsFields: [],
|
||||
});
|
||||
expect(isDisabled).toEqual(true);
|
||||
});
|
||||
|
||||
test('disabled when there is no threat index', () => {
|
||||
const isDisabled = getIsRulePreviewDisabled({
|
||||
ruleType: 'threat_match',
|
||||
isQueryBarValid: true,
|
||||
isThreatQueryBarValid: true,
|
||||
index: ['test-*'],
|
||||
dataViewId: undefined,
|
||||
dataSourceType: DataSourceType.IndexPatterns,
|
||||
threatIndex: [],
|
||||
threatMapping: [
|
||||
{ entries: [{ field: 'test-field', value: 'test-value', type: 'mapping' }] },
|
||||
],
|
||||
machineLearningJobId: ['test-ml-job-id'],
|
||||
queryBar: { filters: [], query: { query: '', language: 'testlang' }, saved_id: null },
|
||||
newTermsFields: [],
|
||||
});
|
||||
expect(isDisabled).toEqual(true);
|
||||
});
|
||||
|
||||
test('disabled when there is no threat mapping', () => {
|
||||
const isDisabled = getIsRulePreviewDisabled({
|
||||
ruleType: 'threat_match',
|
||||
isQueryBarValid: true,
|
||||
isThreatQueryBarValid: true,
|
||||
index: ['test-*'],
|
||||
dataViewId: undefined,
|
||||
dataSourceType: DataSourceType.IndexPatterns,
|
||||
threatIndex: ['threat-*'],
|
||||
threatMapping: [],
|
||||
machineLearningJobId: ['test-ml-job-id'],
|
||||
queryBar: { filters: [], query: { query: '', language: 'testlang' }, saved_id: null },
|
||||
newTermsFields: [],
|
||||
});
|
||||
expect(isDisabled).toEqual(true);
|
||||
});
|
||||
|
||||
test('disabled when there is no machine learning job id', () => {
|
||||
const isDisabled = getIsRulePreviewDisabled({
|
||||
ruleType: 'threat_match',
|
||||
isQueryBarValid: true,
|
||||
isThreatQueryBarValid: true,
|
||||
index: ['test-*'],
|
||||
dataViewId: undefined,
|
||||
dataSourceType: DataSourceType.IndexPatterns,
|
||||
threatIndex: ['threat-*'],
|
||||
threatMapping: [],
|
||||
machineLearningJobId: [],
|
||||
queryBar: { filters: [], query: { query: '', language: 'testlang' }, saved_id: null },
|
||||
newTermsFields: [],
|
||||
});
|
||||
expect(isDisabled).toEqual(true);
|
||||
});
|
||||
|
||||
test('disabled when eql rule with no query', () => {
|
||||
const isDisabled = getIsRulePreviewDisabled({
|
||||
ruleType: 'eql',
|
||||
isQueryBarValid: true,
|
||||
isThreatQueryBarValid: true,
|
||||
index: ['test-*'],
|
||||
dataViewId: undefined,
|
||||
dataSourceType: DataSourceType.IndexPatterns,
|
||||
threatIndex: ['threat-*'],
|
||||
threatMapping: [],
|
||||
machineLearningJobId: [],
|
||||
queryBar: { filters: [], query: { query: '', language: 'testlang' }, saved_id: null },
|
||||
newTermsFields: [],
|
||||
});
|
||||
expect(isDisabled).toEqual(true);
|
||||
});
|
||||
|
||||
test('disabled when new_terms rule with no fields', () => {
|
||||
const isDisabled = getIsRulePreviewDisabled({
|
||||
ruleType: 'new_terms',
|
||||
isQueryBarValid: true,
|
||||
isThreatQueryBarValid: true,
|
||||
index: ['test-*'],
|
||||
dataViewId: undefined,
|
||||
dataSourceType: DataSourceType.IndexPatterns,
|
||||
threatIndex: [],
|
||||
threatMapping: [],
|
||||
machineLearningJobId: [],
|
||||
queryBar: { filters: [], query: { query: '', language: 'testlang' }, saved_id: null },
|
||||
newTermsFields: [],
|
||||
});
|
||||
expect(isDisabled).toEqual(true);
|
||||
});
|
||||
|
||||
test('enabled', () => {
|
||||
const isDisabled = getIsRulePreviewDisabled({
|
||||
ruleType: 'threat_match',
|
||||
isQueryBarValid: true,
|
||||
isThreatQueryBarValid: true,
|
||||
index: ['test-*'],
|
||||
dataViewId: undefined,
|
||||
dataSourceType: DataSourceType.IndexPatterns,
|
||||
threatIndex: ['threat-*'],
|
||||
threatMapping: [
|
||||
{ entries: [{ field: 'test-field', value: 'test-value', type: 'mapping' }] },
|
||||
],
|
||||
machineLearningJobId: ['test-ml-job-id'],
|
||||
queryBar: { filters: [], query: { query: '', language: 'testlang' }, saved_id: null },
|
||||
newTermsFields: [],
|
||||
});
|
||||
expect(isDisabled).toEqual(false);
|
||||
});
|
||||
|
||||
test('enabled when eql rule with query', () => {
|
||||
const isDisabled = getIsRulePreviewDisabled({
|
||||
ruleType: 'eql',
|
||||
isQueryBarValid: true,
|
||||
isThreatQueryBarValid: true,
|
||||
index: ['test-*'],
|
||||
dataViewId: undefined,
|
||||
dataSourceType: DataSourceType.IndexPatterns,
|
||||
threatIndex: ['threat-*'],
|
||||
threatMapping: [],
|
||||
machineLearningJobId: [],
|
||||
queryBar: {
|
||||
filters: [],
|
||||
query: { query: 'any where true', language: 'testlang' },
|
||||
saved_id: null,
|
||||
},
|
||||
newTermsFields: [],
|
||||
});
|
||||
expect(isDisabled).toEqual(false);
|
||||
});
|
||||
|
||||
test('disabled when eql rule with empty query and non-empty filters', () => {
|
||||
const isDisabled = getIsRulePreviewDisabled({
|
||||
ruleType: 'eql',
|
||||
isQueryBarValid: true,
|
||||
isThreatQueryBarValid: false,
|
||||
index: ['test-*'],
|
||||
dataViewId: undefined,
|
||||
dataSourceType: DataSourceType.IndexPatterns,
|
||||
threatIndex: [],
|
||||
threatMapping: [],
|
||||
machineLearningJobId: [],
|
||||
queryBar: {
|
||||
filters: [
|
||||
{
|
||||
meta: {},
|
||||
query: {
|
||||
exists: {
|
||||
field: '_index',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
query: { query: '', language: 'eql' },
|
||||
saved_id: null,
|
||||
},
|
||||
newTermsFields: [],
|
||||
});
|
||||
expect(isDisabled).toEqual(true);
|
||||
});
|
||||
|
||||
test('disabled when eql rule with empty query and empty filters', () => {
|
||||
const isDisabled = getIsRulePreviewDisabled({
|
||||
ruleType: 'eql',
|
||||
isQueryBarValid: true,
|
||||
isThreatQueryBarValid: false,
|
||||
index: ['test-*'],
|
||||
dataViewId: undefined,
|
||||
dataSourceType: DataSourceType.IndexPatterns,
|
||||
threatIndex: [],
|
||||
threatMapping: [],
|
||||
machineLearningJobId: [],
|
||||
queryBar: {
|
||||
filters: [],
|
||||
query: { query: '', language: 'eql' },
|
||||
saved_id: null,
|
||||
},
|
||||
newTermsFields: [],
|
||||
});
|
||||
expect(isDisabled).toEqual(true);
|
||||
});
|
||||
|
||||
test('enabled when eql rule with non empty query', () => {
|
||||
const isDisabled = getIsRulePreviewDisabled({
|
||||
ruleType: 'eql',
|
||||
isQueryBarValid: true,
|
||||
isThreatQueryBarValid: false,
|
||||
index: ['test-*'],
|
||||
dataViewId: undefined,
|
||||
dataSourceType: DataSourceType.IndexPatterns,
|
||||
threatIndex: [],
|
||||
threatMapping: [],
|
||||
machineLearningJobId: [],
|
||||
queryBar: {
|
||||
filters: [],
|
||||
query: { query: 'any where true', language: 'eql' },
|
||||
saved_id: null,
|
||||
},
|
||||
newTermsFields: [],
|
||||
});
|
||||
expect(isDisabled).toEqual(false);
|
||||
});
|
||||
|
||||
// ML rule does not have index or data view id properties, so preview should not depend on these fields
|
||||
test('enabled for ML rule when index patterns and data view id are empty', () => {
|
||||
const isDisabled = getIsRulePreviewDisabled({
|
||||
ruleType: 'machine_learning',
|
||||
isQueryBarValid: true,
|
||||
isThreatQueryBarValid: true,
|
||||
index: [],
|
||||
dataViewId: undefined,
|
||||
dataSourceType: DataSourceType.IndexPatterns,
|
||||
threatIndex: [],
|
||||
threatMapping: [],
|
||||
machineLearningJobId: ['test-ml-job-id'],
|
||||
queryBar: { filters: [], query: { query: '', language: '' }, saved_id: null },
|
||||
newTermsFields: [],
|
||||
});
|
||||
expect(isDisabled).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTimeframeOptions', () => {
|
||||
test('returns hour and day options if ruleType is eql', () => {
|
||||
const options = getTimeframeOptions('eql');
|
||||
|
|
|
@ -5,16 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
import type { EuiSelectOption } from '@elastic/eui';
|
||||
import type { Type, ThreatMapping } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import type { Type } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
import type { FieldValueQueryBar } from '../query_bar_field';
|
||||
import type { TimeframePreviewOptions } from '../../../../detections/pages/detection_engine/rules/types';
|
||||
import { DataSourceType } from '../../../../detections/pages/detection_engine/rules/types';
|
||||
import { MAX_NUMBER_OF_NEW_TERMS_FIELDS } from '../../../../../common/constants';
|
||||
|
||||
/**
|
||||
* Determines whether or not to display noise warning.
|
||||
* Is considered noisy if alerts/hour rate > 1
|
||||
|
@ -58,97 +53,3 @@ export const getTimeframeOptions = (ruleType: Type): EuiSelectOption[] => {
|
|||
];
|
||||
}
|
||||
};
|
||||
|
||||
const isNewTermsPreviewDisabled = (newTermsFields: string[]): boolean => {
|
||||
return newTermsFields.length === 0 || newTermsFields.length > MAX_NUMBER_OF_NEW_TERMS_FIELDS;
|
||||
};
|
||||
|
||||
const isEsqlPreviewDisabled = ({
|
||||
isQueryBarValid,
|
||||
queryBar,
|
||||
}: {
|
||||
queryBar: FieldValueQueryBar;
|
||||
isQueryBarValid: boolean;
|
||||
}): boolean => {
|
||||
return !isQueryBarValid || isEmpty(queryBar.query.query);
|
||||
};
|
||||
|
||||
const isThreatMatchPreviewDisabled = ({
|
||||
isThreatQueryBarValid,
|
||||
threatIndex,
|
||||
threatMapping,
|
||||
}: {
|
||||
threatIndex: string[];
|
||||
threatMapping: ThreatMapping;
|
||||
isThreatQueryBarValid: boolean;
|
||||
}): boolean => {
|
||||
if (!isThreatQueryBarValid || !threatIndex.length || !threatMapping) {
|
||||
return true;
|
||||
} else if (
|
||||
!threatMapping.length ||
|
||||
!threatMapping[0].entries?.length ||
|
||||
!threatMapping[0].entries[0].field ||
|
||||
!threatMapping[0].entries[0].value
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getIsRulePreviewDisabled = ({
|
||||
ruleType,
|
||||
isQueryBarValid,
|
||||
isThreatQueryBarValid,
|
||||
index,
|
||||
dataViewId,
|
||||
dataSourceType,
|
||||
threatIndex,
|
||||
threatMapping,
|
||||
machineLearningJobId,
|
||||
queryBar,
|
||||
newTermsFields = [],
|
||||
}: {
|
||||
ruleType: Type;
|
||||
isQueryBarValid: boolean;
|
||||
isThreatQueryBarValid: boolean;
|
||||
index: string[];
|
||||
dataViewId: string | undefined;
|
||||
dataSourceType: DataSourceType;
|
||||
threatIndex: string[];
|
||||
threatMapping: ThreatMapping;
|
||||
machineLearningJobId: string[] | undefined;
|
||||
queryBar: FieldValueQueryBar;
|
||||
newTermsFields: string[];
|
||||
}) => {
|
||||
if (ruleType === 'esql') {
|
||||
return isEsqlPreviewDisabled({ isQueryBarValid, queryBar });
|
||||
}
|
||||
if (ruleType === 'machine_learning') {
|
||||
return !machineLearningJobId ?? machineLearningJobId?.length === 0;
|
||||
}
|
||||
if (
|
||||
!isQueryBarValid ||
|
||||
(dataSourceType === DataSourceType.DataView && !dataViewId) ||
|
||||
(dataSourceType === DataSourceType.IndexPatterns && index.length === 0)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (ruleType === 'threat_match') {
|
||||
return isThreatMatchPreviewDisabled({
|
||||
threatIndex,
|
||||
threatMapping,
|
||||
isThreatQueryBarValid,
|
||||
});
|
||||
}
|
||||
if (ruleType === 'eql') {
|
||||
return isEmpty(queryBar.query.query);
|
||||
}
|
||||
if (ruleType === 'query' || ruleType === 'threshold') {
|
||||
return isEmpty(queryBar.query.query) && isEmpty(queryBar.filters);
|
||||
}
|
||||
if (ruleType === 'new_terms') {
|
||||
return isNewTermsPreviewDisabled(newTermsFields);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
|
|
@ -39,6 +39,8 @@ jest.mock('../../../../common/hooks/use_experimental_features', () => ({
|
|||
useIsExperimentalFeatureEnabled: jest.fn(),
|
||||
}));
|
||||
|
||||
const verifyRuleDefinitionMock = jest.fn().mockResolvedValue(true);
|
||||
|
||||
// rule types that do not support logged requests
|
||||
const doNotSupportLoggedRequests: Type[] = ['threat_match'];
|
||||
|
||||
|
@ -59,6 +61,7 @@ const getMockIndexPattern = (): DataViewBase => ({
|
|||
});
|
||||
|
||||
const defaultProps: RulePreviewProps = {
|
||||
verifyRuleDefinition: verifyRuleDefinitionMock,
|
||||
defineRuleData: {
|
||||
...stepDefineDefaultValue,
|
||||
ruleType: 'threat_match',
|
||||
|
@ -155,6 +158,18 @@ describe('PreviewQuery', () => {
|
|||
expect(await wrapper.findByTestId('previewInvocationCountWarning')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it renders a warning when the definition step form is invalid', async () => {
|
||||
verifyRuleDefinitionMock.mockResolvedValueOnce(false);
|
||||
|
||||
const wrapper = render(
|
||||
<TestProviders>
|
||||
<RulePreview {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
(await wrapper.findByTestId('previewSubmitButton')).click();
|
||||
expect(await wrapper.findByTestId('previewRuleDefinitionInvalidWarning')).toBeTruthy();
|
||||
});
|
||||
|
||||
supportLoggedRequests.forEach((ruleType) => {
|
||||
test(`renders "Show Elasticsearch requests" for ${ruleType} rule type`, () => {
|
||||
render(
|
||||
|
|
|
@ -65,7 +65,7 @@ const timeRanges = [
|
|||
];
|
||||
|
||||
export interface RulePreviewProps {
|
||||
isDisabled?: boolean;
|
||||
verifyRuleDefinition: () => Promise<boolean>;
|
||||
defineRuleData: DefineStepRule;
|
||||
aboutRuleData: AboutStepRule;
|
||||
scheduleRuleData: ScheduleStepRule;
|
||||
|
@ -88,7 +88,7 @@ const refreshedTimeframe = (startDate: string, endDate: string) => {
|
|||
};
|
||||
|
||||
const RulePreviewComponent: React.FC<RulePreviewProps> = ({
|
||||
isDisabled,
|
||||
verifyRuleDefinition,
|
||||
defineRuleData,
|
||||
aboutRuleData,
|
||||
scheduleRuleData,
|
||||
|
@ -116,6 +116,8 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
|
|||
|
||||
const [isDateRangeInvalid, setIsDateRangeInvalid] = useState(false);
|
||||
|
||||
const [showRuleDefitnionInvalidWarning, setShowRuleDefinitionInvalidWarning] = useState(false);
|
||||
|
||||
const isLoggedRequestsSupported = RULE_TYPES_SUPPORTING_LOGGED_REQUESTS.includes(ruleType);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -189,7 +191,7 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
|
|||
[]
|
||||
);
|
||||
|
||||
const onTimeframeRefresh = useCallback(() => {
|
||||
const triggerTimeFrameRefresh = useCallback(() => {
|
||||
startTransaction({ name: SINGLE_RULE_ACTIONS.PREVIEW });
|
||||
const { start, end } = refreshedTimeframe(startDate, endDate);
|
||||
setTimeframeStart(start);
|
||||
|
@ -218,6 +220,22 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
|
|||
isLoggedRequestsSupported,
|
||||
]);
|
||||
|
||||
const onPreviewTriggered = useCallback(() => {
|
||||
setIsRefreshing(true);
|
||||
const doVerification = async () => {
|
||||
const isValid = await verifyRuleDefinition();
|
||||
if (!isValid) {
|
||||
setShowRuleDefinitionInvalidWarning(true);
|
||||
setIsRefreshing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setShowRuleDefinitionInvalidWarning(false);
|
||||
triggerTimeFrameRefresh();
|
||||
};
|
||||
doVerification();
|
||||
}, [triggerTimeFrameRefresh, verifyRuleDefinition]);
|
||||
|
||||
const isDirty = useMemo(
|
||||
() =>
|
||||
!timeframeStart.isSame(previewData.timeframeOptions.timeframeStart) ||
|
||||
|
@ -261,6 +279,18 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
|
|||
<EuiSpacer />
|
||||
</>
|
||||
)}
|
||||
{showRuleDefitnionInvalidWarning && (
|
||||
<>
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
title={i18n.QUERY_PREVIEW_RULE_DEFINITION_INVALID_WARNING_TITLE}
|
||||
data-test-subj="previewRuleDefinitionInvalidWarning"
|
||||
>
|
||||
{i18n.QUERY_PREVIEW_RULE_DEFINITION_INVALID_WARNING_MESSAGE}
|
||||
</EuiCallOut>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
)}
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiFormRow label={i18n.QUERY_PREVIEW_LABEL}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive>
|
||||
|
@ -268,20 +298,19 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
|
|||
<EuiSuperDatePicker
|
||||
start={startDate}
|
||||
end={endDate}
|
||||
isDisabled={isDisabled}
|
||||
onTimeChange={onTimeChange}
|
||||
showUpdateButton={false}
|
||||
commonlyUsedRanges={timeRanges}
|
||||
onRefresh={onTimeframeRefresh}
|
||||
onRefresh={onPreviewTriggered}
|
||||
data-test-subj="preview-time-frame"
|
||||
width="full"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSuperUpdateButton
|
||||
isDisabled={isDateRangeInvalid || isDisabled}
|
||||
isDisabled={isDateRangeInvalid}
|
||||
iconType={isDirty ? 'kqlFunction' : 'refresh'}
|
||||
onClick={onTimeframeRefresh}
|
||||
onClick={onPreviewTriggered}
|
||||
color={isDirty ? 'success' : 'primary'}
|
||||
fill={true}
|
||||
data-test-subj="previewSubmitButton"
|
||||
|
|
|
@ -86,6 +86,20 @@ export const QUERY_PREVIEW_INVOCATION_COUNT_WARNING_MESSAGE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const QUERY_PREVIEW_RULE_DEFINITION_INVALID_WARNING_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewRuleDefinitionInvalidWarningTitle',
|
||||
{
|
||||
defaultMessage: 'Rule definition is invalid',
|
||||
}
|
||||
);
|
||||
|
||||
export const QUERY_PREVIEW_RULE_DEFINITION_INVALID_WARNING_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewRuleDefinitionInvalidWarningMessage',
|
||||
{
|
||||
defaultMessage: `One or more rule definition fields have validation errors that must be fixed before previewing this rule. To learn more, check the error messages for the invalid rule definition fields.`,
|
||||
}
|
||||
);
|
||||
|
||||
export const QUERY_GRAPH_COUNT = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.queryPreview.queryGraphCountLabel',
|
||||
{
|
||||
|
|
|
@ -78,7 +78,6 @@ import {
|
|||
} from '../../../../../common/constants';
|
||||
import { useKibana, useUiSetting$ } from '../../../../common/lib/kibana';
|
||||
import { RulePreview } from '../../components/rule_preview';
|
||||
import { getIsRulePreviewDisabled } from '../../components/rule_preview/helpers';
|
||||
import { useStartMlJobs } from '../../../rule_management/logic/use_start_ml_jobs';
|
||||
import { VALIDATION_WARNING_CODE_FIELD_NAME_MAP } from '../../../rule_creation/constants/validation_warning_codes';
|
||||
import { extractValidationMessages } from '../../../rule_creation/logic/extract_validation_messages';
|
||||
|
@ -219,24 +218,6 @@ const CreateRulePageComponent: React.FC = () => {
|
|||
|
||||
const defineFieldsTransform = useExperimentalFeatureFieldsTransform<DefineStepRule>();
|
||||
|
||||
const defineStepFormFields = defineStepForm.getFields();
|
||||
const isPreviewDisabled = getIsRulePreviewDisabled({
|
||||
ruleType,
|
||||
isQueryBarValid,
|
||||
isThreatQueryBarValid:
|
||||
defineStepFormFields.threatIndex?.isValid &&
|
||||
defineStepFormFields.threatQueryBar?.isValid &&
|
||||
defineStepFormFields.threatMapping?.isValid,
|
||||
index: memoizedIndex,
|
||||
dataViewId: defineStepData.dataViewId,
|
||||
dataSourceType: defineStepData.dataSourceType,
|
||||
threatIndex: defineStepData.threatIndex,
|
||||
threatMapping: defineStepData.threatMapping,
|
||||
machineLearningJobId: defineStepData.machineLearningJobId,
|
||||
queryBar: defineStepData.queryBar,
|
||||
newTermsFields: defineStepData.newTermsFields,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (prevRuleType !== ruleType) {
|
||||
aboutStepForm.updateFieldValues({
|
||||
|
@ -380,6 +361,11 @@ const CreateRulePageComponent: React.FC = () => {
|
|||
return { valid, warnings };
|
||||
}, [validateStep]);
|
||||
|
||||
const verifyRuleDefinitionForPreview = useCallback(
|
||||
() => defineStepForm.validate(),
|
||||
[defineStepForm]
|
||||
);
|
||||
|
||||
const editStep = useCallback(
|
||||
async (step: RuleStep) => {
|
||||
const { valid } = await validateStep(activeStep);
|
||||
|
@ -921,7 +907,7 @@ const CreateRulePageComponent: React.FC = () => {
|
|||
onToggleCollapsed={onToggleCollapsedMemo}
|
||||
>
|
||||
<RulePreview
|
||||
isDisabled={isPreviewDisabled && activeStep === RuleStep.defineRule}
|
||||
verifyRuleDefinition={verifyRuleDefinitionForPreview}
|
||||
defineRuleData={defineStepData}
|
||||
aboutRuleData={aboutStepData}
|
||||
scheduleRuleData={scheduleStepData}
|
||||
|
|
|
@ -28,7 +28,6 @@ import { useConfirmValidationErrorsModal } from '../../../../common/hooks/use_co
|
|||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
import { isEsqlRule } from '../../../../../common/detection_engine/utils';
|
||||
import { RulePreview } from '../../components/rule_preview';
|
||||
import { getIsRulePreviewDisabled } from '../../components/rule_preview/helpers';
|
||||
import type {
|
||||
RuleResponse,
|
||||
RuleUpdateProps,
|
||||
|
@ -154,24 +153,6 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
|
|||
[defineStepData.index, esqlIndex, defineStepData.ruleType]
|
||||
);
|
||||
|
||||
const defineStepFormFields = defineStepForm.getFields();
|
||||
const isPreviewDisabled = getIsRulePreviewDisabled({
|
||||
ruleType: defineStepData.ruleType,
|
||||
isQueryBarValid,
|
||||
isThreatQueryBarValid:
|
||||
defineStepFormFields.threatIndex?.isValid &&
|
||||
defineStepFormFields.threatQueryBar?.isValid &&
|
||||
defineStepFormFields.threatMapping?.isValid,
|
||||
index: memoizedIndex,
|
||||
dataViewId: defineStepData.dataViewId,
|
||||
dataSourceType: defineStepData.dataSourceType,
|
||||
threatIndex: defineStepData.threatIndex,
|
||||
threatMapping: defineStepData.threatMapping,
|
||||
machineLearningJobId: defineStepData.machineLearningJobId,
|
||||
queryBar: defineStepData.queryBar,
|
||||
newTermsFields: defineStepData.newTermsFields,
|
||||
});
|
||||
|
||||
const loading = userInfoLoading || listsConfigLoading;
|
||||
const { isSavedQueryLoading, savedQuery } = useGetSavedQuery({
|
||||
savedQueryId: 'saved_id' in rule ? rule.saved_id : undefined,
|
||||
|
@ -519,6 +500,11 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
|
|||
),
|
||||
});
|
||||
|
||||
const verifyRuleDefinitionForPreview = useCallback(
|
||||
() => defineStepForm.validate(),
|
||||
[defineStepForm]
|
||||
);
|
||||
|
||||
if (
|
||||
redirectToDetections(
|
||||
isSignalIndexExists,
|
||||
|
@ -631,7 +617,7 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
|
|||
onToggleCollapsed={() => setIsRulePreviewVisible((isVisible) => !isVisible)}
|
||||
>
|
||||
<RulePreview
|
||||
isDisabled={isPreviewDisabled}
|
||||
verifyRuleDefinition={verifyRuleDefinitionForPreview}
|
||||
defineRuleData={defineStepData}
|
||||
aboutRuleData={aboutStepData}
|
||||
scheduleRuleData={scheduleStepData}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue