[Security Solution][Detection Engine] fixes empty EQL query validation (#212117)

## Summary

- addresses https://github.com/elastic/kibana/issues/201778
This commit is contained in:
Vitalii Dmyterko 2025-02-26 14:59:52 +00:00 committed by GitHub
parent 0eb08ccc05
commit fd1a0a9b95
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 302 additions and 32 deletions

View file

@ -269,6 +269,78 @@ describe('query_preview/helpers', () => {
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({

View file

@ -141,7 +141,10 @@ export const getIsRulePreviewDisabled = ({
isThreatQueryBarValid,
});
}
if (ruleType === 'eql' || ruleType === 'query' || ruleType === 'threshold') {
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') {

View file

@ -794,6 +794,186 @@ describe('StepDefineRule', () => {
expect(screen.queryByTestId('ai-assistant')).toBe(null);
});
});
describe('query validation', () => {
describe('Query rule', () => {
it('shows query is required when filters and query empty', async () => {
const initialState = {
queryBar: {
query: { query: '', language: 'kuery' },
filters: [],
saved_id: null,
},
};
render(<TestForm formProps={{ isQueryBarValid: false }} initialState={initialState} />, {
wrapper: TestProviders,
});
await submitForm();
await waitFor(() => {
expect(screen.getByTestId('detectionEngineStepDefineRuleQueryBar')).toHaveTextContent(
'A custom query is required'
);
});
});
it('does not show query is required when filters not empty and query empty', async () => {
const initialState = {
queryBar: {
query: { query: '', language: 'kuery' },
filters: [
{
meta: {},
query: {
exists: {
field: '_index',
},
},
},
],
saved_id: null,
},
};
render(<TestForm formProps={{ isQueryBarValid: false }} initialState={initialState} />, {
wrapper: TestProviders,
});
await submitForm();
await expect(
waitFor(() => {
expect(screen.getByTestId('detectionEngineStepDefineRuleQueryBar')).toHaveTextContent(
'A custom query is required'
);
})
).rejects.toThrow();
});
});
describe('ES|QL rule', () => {
it('shows ES|QL query is required when it is empty', async () => {
const initialState = {
queryBar: {
query: { query: '', language: 'esql' },
filters: [],
saved_id: null,
},
ruleType: 'esql' as const,
};
render(<TestForm formProps={{ isQueryBarValid: false }} initialState={initialState} />, {
wrapper: TestProviders,
});
await submitForm();
await waitFor(() => {
expect(screen.getByTestId('ruleEsqlQueryBar')).toHaveTextContent(
'ES|QL query is required'
);
});
});
it('does not show ES|QL query is required when it is not empty', async () => {
const initialState = {
queryBar: {
query: { query: 'from my_index metadata _id', language: 'esql' },
filters: [],
saved_id: null,
},
ruleType: 'esql' as const,
};
render(<TestForm formProps={{ isQueryBarValid: false }} initialState={initialState} />, {
wrapper: TestProviders,
});
await submitForm();
await expect(
waitFor(() => {
expect(screen.getByTestId('ruleEsqlQueryBar')).toHaveTextContent(
'ES|QL query is required'
);
})
).rejects.toThrow();
});
});
describe('EQL rule', () => {
it('shows EQL query is required when it is empty', async () => {
const initialState = {
queryBar: {
query: { query: '', language: 'eql' },
filters: [],
saved_id: null,
},
ruleType: 'eql' as const,
};
render(<TestForm formProps={{ isQueryBarValid: false }} initialState={initialState} />, {
wrapper: TestProviders,
});
await submitForm();
await waitFor(() => {
expect(screen.getByTestId('ruleEqlQueryBar')).toHaveTextContent('EQL query is required');
});
});
it('shows EQL query is required when query empty, but filters non-empty', async () => {
const initialState = {
queryBar: {
query: { query: '', language: 'eql' },
filters: [
{
meta: {},
query: {
exists: {
field: '_index',
},
},
},
],
saved_id: null,
},
ruleType: 'eql' as const,
};
render(<TestForm formProps={{ isQueryBarValid: false }} initialState={initialState} />, {
wrapper: TestProviders,
});
await submitForm();
await waitFor(() => {
expect(screen.getByTestId('ruleEqlQueryBar')).toHaveTextContent('EQL query is required');
});
});
it('does not show EQL query is required when it is not empty', async () => {
const initialState = {
queryBar: {
query: { query: 'any where true', language: 'eql' },
filters: [],
saved_id: null,
},
ruleType: 'eql' as const,
};
render(<TestForm formProps={{ isQueryBarValid: false }} initialState={initialState} />, {
wrapper: TestProviders,
});
await submitForm();
await expect(
waitFor(() => {
expect(screen.getByTestId('detectionEngineStepDefineRuleQueryBar')).toHaveTextContent(
'EQL query is required'
);
})
).rejects.toThrow();
});
});
});
});
interface TestFormProps {

View file

@ -12,45 +12,60 @@ import type { FormData, ValidationFunc } from '../../../shared_imports';
import { isEqlRule, isEsqlRule } from '../../../../common/detection_engine/utils';
import type { FieldValueQueryBar } from '../components/query_bar_field';
const EQL_REQUIRED = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.eqlQueryFieldRequiredError',
{
defaultMessage: 'An EQL query is required.',
}
);
const ESQL_REQUIRED = i18n.translate(
'xpack.securitySolution.ruleManagement.ruleCreation.validation.query.esqlQueryFieldRequiredError',
{
defaultMessage: 'An ES|QL query is required.',
}
);
const CUSTOM_QUERY_REQUIRED = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customQueryFieldRequiredError',
{
defaultMessage: 'A custom query is required.',
}
);
export function queryRequiredValidatorFactory(
ruleType: RuleType
): ValidationFunc<FormData, string, FieldValueQueryBar> {
return (...args) => {
const [{ path, value }] = args;
const validationError = {
code: 'ERR_FIELD_MISSING',
path,
};
if (isEmpty(value.query.query as string) && isEmpty(value.filters)) {
if (!isEmpty(value.query.query as string)) {
return;
}
if (isEqlRule(ruleType)) {
return {
code: 'ERR_FIELD_MISSING',
path,
message: getErrorMessage(ruleType),
...validationError,
message: EQL_REQUIRED,
};
}
if (isEsqlRule(ruleType)) {
return {
...validationError,
message: ESQL_REQUIRED,
};
}
if (isEmpty(value.filters)) {
return {
...validationError,
message: CUSTOM_QUERY_REQUIRED,
};
}
};
}
function getErrorMessage(ruleType: RuleType): string {
if (isEsqlRule(ruleType)) {
return i18n.translate(
'xpack.securitySolution.ruleManagement.ruleCreation.validation.query.esqlQueryFieldRequiredError',
{
defaultMessage: 'An ES|QL query is required.',
}
);
}
if (isEqlRule(ruleType)) {
return i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.eqlQueryFieldRequiredError',
{
defaultMessage: 'An EQL query is required.',
}
);
}
return i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customQueryFieldRequiredError',
{
defaultMessage: 'A custom query is required.',
}
);
}