Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Security Solution][Detection Engine] adds alert suppression to ES|QL rule type #180927

Merged
merged 77 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
04bb741
[Security Solution][Detection Engine] adds alert suppression to ES|QL…
vitaliidm Apr 16, 2024
07cd5c6
add per rule execution tests
vitaliidm Apr 16, 2024
5964b10
add cypress tests
vitaliidm Apr 16, 2024
8cc1cc5
add alert suppresson schema to esql
vitaliidm Apr 16, 2024
76a49ad
add FF
vitaliidm Apr 16, 2024
2e52dc5
introduce BE suppression
vitaliidm Apr 17, 2024
0ffd02d
Merge branch 'main' into de_8_15/esql_alert_suppression
vitaliidm Apr 17, 2024
9e5c965
add interim BE implementation
vitaliidm Apr 17, 2024
41dbdbe
fix tests
vitaliidm Apr 17, 2024
53c4ef3
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Apr 17, 2024
4235d2b
fix lint errors
vitaliidm Apr 17, 2024
8158978
fix few corner cases
vitaliidm Apr 17, 2024
ce9ef01
refactoring
vitaliidm Apr 17, 2024
5b0e7da
Merge branch 'main' into de_8_15/esql_alert_suppression
vitaliidm Apr 18, 2024
e7eabd3
Merge branch 'main' into de_8_15/esql_alert_suppression
vitaliidm Apr 18, 2024
5cecc2c
Merge branch 'main' into de_8_15/esql_alert_suppression
vitaliidm Apr 18, 2024
728f037
Merge branch 'main' into de_8_15/esql_alert_suppression
vitaliidm Apr 26, 2024
5f03eb7
add use_suppression
vitaliidm Apr 30, 2024
76c9f83
Merge branch 'main' into de_8_15/esql_alert_suppression
vitaliidm Apr 30, 2024
08acc15
Merge branch 'main' into de_8_15/esql_alert_suppression
vitaliidm May 1, 2024
2062464
fix tests
vitaliidm May 1, 2024
39ac37b
Merge branch 'main' into de_8_15/esql_alert_suppression
vitaliidm May 1, 2024
f73511d
refactor useSuppression
vitaliidm May 1, 2024
b89b795
Merge branch 'de_8_15/esql_alert_suppression' of https://github.com/v…
vitaliidm May 1, 2024
be63c57
Merge branch 'main' into de_8_15/esql_alert_suppression
vitaliidm May 2, 2024
c48939f
Merge branch 'main' into de_8_15/esql_alert_suppression
vitaliidm May 2, 2024
f3c7146
fix cypress tests
vitaliidm May 2, 2024
5dcf7ed
Merge branch 'de_8_15/esql_alert_suppression' of https://github.com/v…
vitaliidm May 2, 2024
d7bc61a
add unit tests to generate alert id
vitaliidm May 2, 2024
3123017
add more unit tests
vitaliidm May 2, 2024
849d974
Merge branch 'main' into de_8_15/esql_alert_suppression
vitaliidm May 2, 2024
95a6e59
add one more unit test
vitaliidm May 3, 2024
3a8c909
Merge branch 'de_8_15/esql_alert_suppression' of https://github.com/v…
vitaliidm May 3, 2024
699b2b9
add unit tests
vitaliidm May 3, 2024
e578a36
fix old metadata syntax
vitaliidm May 3, 2024
d568f6d
Merge branch 'main' into de_8_15/esql_alert_suppression
vitaliidm May 3, 2024
182a15f
fix skipped tests
vitaliidm May 3, 2024
2028563
Merge branch 'de_8_15/esql_alert_suppression' of https://github.com/v…
vitaliidm May 3, 2024
492e0d9
Merge branch 'main' into de_8_15/esql_alert_suppression
vitaliidm May 7, 2024
2b4a98a
ftr
vitaliidm May 7, 2024
dde29cd
Merge branch 'de_8_15/esql_alert_suppression' of https://github.com/v…
vitaliidm May 7, 2024
753d155
Update esql_suppression.ts
vitaliidm May 7, 2024
5485299
enable serverless tests
vitaliidm May 7, 2024
b749a45
runs ES|QL cypress tests only for flaky test runner
vitaliidm May 7, 2024
df45873
Revert "runs ES|QL cypress tests only for flaky test runner"
vitaliidm May 7, 2024
ae631fa
Merge branch 'main' into de_8_15/esql_alert_suppression
vitaliidm May 7, 2024
9252b35
id change
vitaliidm May 8, 2024
5da3911
Merge branch 'de_8_15/esql_alert_suppression' of https://github.com/v…
vitaliidm May 8, 2024
c66cfc6
attempt to fix the test
vitaliidm May 8, 2024
d03a07d
Merge branch 'main' into de_8_15/esql_alert_suppression
vitaliidm May 8, 2024
d3365f8
tie ESQL_PAGE_SIZE_CIRCUIT_BREAKER to max signals
vitaliidm May 8, 2024
6e84891
Merge branch 'de_8_15/esql_alert_suppression' of https://github.com/v…
vitaliidm May 8, 2024
4d42a0f
refacotr
vitaliidm May 8, 2024
fa4298f
fix test
vitaliidm May 8, 2024
afa01ac
Merge branch 'main' into de_8_15/esql_alert_suppression
vitaliidm May 9, 2024
b6f437a
Merge branch 'main' into de_8_15/esql_alert_suppression
vitaliidm May 13, 2024
0cef907
CR
vitaliidm May 14, 2024
131929e
CR: deduplicate and merge fields
vitaliidm May 14, 2024
b61f4b9
Merge branch 'main' into de_8_15/esql_alert_suppression
vitaliidm May 15, 2024
1e30a57
refactor cypress tests
vitaliidm May 16, 2024
252a703
[TEMPORARY] run only esql cypress tests
vitaliidm May 16, 2024
07188d9
Revert "[TEMPORARY] run only esql cypress tests"
vitaliidm May 16, 2024
02290ac
Merge branch 'main' into de_8_15/esql_alert_suppression
vitaliidm May 16, 2024
eb7db73
Update x-pack/plugins/security_solution/public/detection_engine/rule_…
vitaliidm May 16, 2024
ffd5b52
fix ES|QL rule timeline
vitaliidm May 17, 2024
e7aa77d
Merge branch 'de_8_15/esql_alert_suppression' of https://github.com/v…
vitaliidm May 17, 2024
ec235db
Merge branch 'main' into de_8_15/esql_alert_suppression
vitaliidm May 17, 2024
def83f5
refacotr
vitaliidm May 17, 2024
9125e5a
Merge branch 'main' into de_8_15/esql_alert_suppression
vitaliidm May 17, 2024
4fd1fa8
Merge branch 'main' into de_8_15/esql_alert_suppression
vitaliidm May 17, 2024
f981ec6
fix ES|QL cypress tests
vitaliidm May 17, 2024
ee1ed67
Merge branch 'de_8_15/esql_alert_suppression' of https://github.com/v…
vitaliidm May 17, 2024
b13f923
[TEMPORARY] run only esql cypress tests
vitaliidm May 16, 2024
65b6957
Revert "[TEMPORARY] run only esql cypress tests"
vitaliidm May 17, 2024
848c52c
Merge branch 'main' into de_8_15/esql_alert_suppression
vitaliidm May 20, 2024
8702a48
preview tests
vitaliidm May 20, 2024
83bf8dd
Merge branch 'de_8_15/esql_alert_suppression' of https://github.com/v…
vitaliidm May 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,9 @@ describe('getIndexListFromEsqlQuery', () => {
getIndexPatternFromESQLQueryMock.mockReturnValue('test-1 , test-2 ');
expect(getIndexListFromEsqlQuery('From test-1, test-2 ')).toEqual(['test-1', 'test-2']);
});

it('should return empty array when getIndexPatternFromESQLQuery throws error', () => {
getIndexPatternFromESQLQueryMock.mockReturnValue(new Error('Fail to parse'));
expect(getIndexListFromEsqlQuery('From test-1 []')).toEqual([]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils';
* parses ES|QL query and returns array of indices
*/
export const getIndexListFromEsqlQuery = (query: string | undefined): string[] => {
const indexString = getIndexPatternFromESQLQuery(query);
try {
const indexString = getIndexPatternFromESQLQuery(query);

return getIndexListFromIndexString(indexString);
return getIndexListFromIndexString(indexString);
} catch (e) {
yctercero marked this conversation as resolved.
Show resolved Hide resolved
return [];
}
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1267,6 +1267,7 @@ describe('rules schema', () => {
// behaviour common for multiple rule types
const cases = [
{ ruleType: 'threat_match', ruleMock: getCreateThreatMatchRulesSchemaMock() },
{ ruleType: 'esql', ruleMock: getCreateEsqlRulesSchemaMock() },
{ ruleType: 'query', ruleMock: getCreateRulesSchemaMock() },
{ ruleType: 'saved_query', ruleMock: getCreateSavedQueryRulesSchemaMock() },
{ ruleType: 'eql', ruleMock: getCreateEqlRuleSchemaMock() },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -560,14 +560,19 @@ export const EsqlRuleRequiredFields = z.object({
query: RuleQuery,
});

export type EsqlRuleOptionalFields = z.infer<typeof EsqlRuleOptionalFields>;
export const EsqlRuleOptionalFields = z.object({
alert_suppression: AlertSuppression.optional(),
});

export type EsqlRulePatchFields = z.infer<typeof EsqlRulePatchFields>;
export const EsqlRulePatchFields = EsqlRuleRequiredFields.partial();
export const EsqlRulePatchFields = EsqlRuleOptionalFields.merge(EsqlRuleRequiredFields.partial());

export type EsqlRuleResponseFields = z.infer<typeof EsqlRuleResponseFields>;
export const EsqlRuleResponseFields = EsqlRuleRequiredFields;
export const EsqlRuleResponseFields = EsqlRuleOptionalFields.merge(EsqlRuleRequiredFields);

export type EsqlRuleCreateFields = z.infer<typeof EsqlRuleCreateFields>;
export const EsqlRuleCreateFields = EsqlRuleRequiredFields;
export const EsqlRuleCreateFields = EsqlRuleOptionalFields.merge(EsqlRuleRequiredFields);

export type EsqlRule = z.infer<typeof EsqlRule>;
export const EsqlRule = SharedResponseProps.merge(EsqlRuleResponseFields);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -826,17 +826,26 @@ components:
- language
- query

EsqlRuleOptionalFields:
type: object
properties:
alert_suppression:
$ref: './common_attributes.schema.yaml#/components/schemas/AlertSuppression'

EsqlRulePatchFields:
allOf:
- $ref: '#/components/schemas/EsqlRuleOptionalFields'
- $ref: '#/components/schemas/EsqlRuleRequiredFields'
x-modify: partial

EsqlRuleResponseFields:
allOf:
- $ref: '#/components/schemas/EsqlRuleOptionalFields'
- $ref: '#/components/schemas/EsqlRuleRequiredFields'

EsqlRuleCreateFields:
allOf:
- $ref: '#/components/schemas/EsqlRuleOptionalFields'
- $ref: '#/components/schemas/EsqlRuleRequiredFields'

EsqlRule:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const MINIMUM_LICENSE_FOR_SUPPRESSION = 'platinum' as const;

export const SUPPRESSIBLE_ALERT_RULES: Type[] = [
'threshold',
'esql',
'saved_query',
'query',
'new_terms',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ describe('Alert Suppression Rules', () => {
describe('isSuppressibleAlertRule', () => {
test('should return true for a suppressible rule type', () => {
// Rule types that support alert suppression:
expect(isSuppressibleAlertRule('esql')).toBe(true);
expect(isSuppressibleAlertRule('threshold')).toBe(true);
expect(isSuppressibleAlertRule('saved_query')).toBe(true);
expect(isSuppressibleAlertRule('query')).toBe(true);
Expand All @@ -238,7 +239,6 @@ describe('Alert Suppression Rules', () => {

// Rule types that don't support alert suppression:
expect(isSuppressibleAlertRule('machine_learning')).toBe(false);
expect(isSuppressibleAlertRule('esql')).toBe(false);
});

test('should return false for an unknown rule type', () => {
Expand Down Expand Up @@ -266,6 +266,7 @@ describe('Alert Suppression Rules', () => {
describe('isSuppressionRuleConfiguredWithDuration', () => {
test('should return true for a suppressible rule type', () => {
// Rule types that support alert suppression:
expect(isSuppressionRuleConfiguredWithDuration('esql')).toBe(true);
expect(isSuppressionRuleConfiguredWithDuration('threshold')).toBe(true);
expect(isSuppressionRuleConfiguredWithDuration('saved_query')).toBe(true);
expect(isSuppressionRuleConfiguredWithDuration('query')).toBe(true);
Expand All @@ -275,7 +276,6 @@ describe('Alert Suppression Rules', () => {

// Rule types that don't support alert suppression:
expect(isSuppressionRuleConfiguredWithDuration('machine_learning')).toBe(false);
expect(isSuppressionRuleConfiguredWithDuration('esql')).toBe(false);
});

test('should return false for an unknown rule type', () => {
Expand All @@ -288,6 +288,7 @@ describe('Alert Suppression Rules', () => {
describe('isSuppressionRuleConfiguredWithGroupBy', () => {
test('should return true for a suppressible rule type with groupBy', () => {
// Rule types that support alert suppression groupBy:
expect(isSuppressionRuleConfiguredWithGroupBy('esql')).toBe(true);
expect(isSuppressionRuleConfiguredWithGroupBy('saved_query')).toBe(true);
expect(isSuppressionRuleConfiguredWithGroupBy('query')).toBe(true);
expect(isSuppressionRuleConfiguredWithGroupBy('threat_match')).toBe(true);
Expand All @@ -296,7 +297,6 @@ describe('Alert Suppression Rules', () => {

// Rule types that don't support alert suppression:
expect(isSuppressionRuleConfiguredWithGroupBy('machine_learning')).toBe(false);
expect(isSuppressionRuleConfiguredWithGroupBy('esql')).toBe(false);
});

test('should return false for a threshold rule type', () => {
Expand All @@ -314,6 +314,7 @@ describe('Alert Suppression Rules', () => {
describe('isSuppressionRuleConfiguredWithMissingFields', () => {
test('should return true for a suppressible rule type with missing fields', () => {
// Rule types that support alert suppression groupBy:
expect(isSuppressionRuleConfiguredWithMissingFields('esql')).toBe(true);
expect(isSuppressionRuleConfiguredWithMissingFields('saved_query')).toBe(true);
expect(isSuppressionRuleConfiguredWithMissingFields('query')).toBe(true);
expect(isSuppressionRuleConfiguredWithMissingFields('threat_match')).toBe(true);
Expand All @@ -322,7 +323,6 @@ describe('Alert Suppression Rules', () => {

// Rule types that don't support alert suppression:
expect(isSuppressionRuleConfiguredWithMissingFields('machine_learning')).toBe(false);
expect(isSuppressionRuleConfiguredWithMissingFields('esql')).toBe(false);
});

test('should return false for a threshold rule type', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,11 @@ export const allowedExperimentalValues = Object.freeze({
*/
disableTimelineSaveTour: false,

/**
* Enables alerts suppression for ES|QL rules
*/
alertSuppressionForEsqlRuleEnabled: false,

/**
* Enables the risk engine privileges route
* and associated callout in the UI
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ Examples:
│ New Terms │ Custom query │ Overview │ Definition │
│ New Terms │ Filters │ Overview │ Definition │
│ ESQL │ ESQL query │ Overview │ Definition │
│ ESQL │ Suppress alerts by │ Overview │ Definition │
│ ESQL │ Suppress alerts for │ Overview │ Definition │
│ ESQL │ If a suppression field is missing │ Overview │ Definition │
```

## Scenarios
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@
* 2.0.
*/

import { computeHasMetadataOperator } from './esql_validator';
import { parseEsqlQuery, computeHasMetadataOperator } from './esql_validator';

import { computeIsESQLQueryAggregating } from '@kbn/securitysolution-utils';

jest.mock('@kbn/securitysolution-utils', () => ({ computeIsESQLQueryAggregating: jest.fn() }));

const computeIsESQLQueryAggregatingMock = computeIsESQLQueryAggregating as jest.Mock;

describe('computeHasMetadataOperator', () => {
it('should be false if query does not have operator', () => {
Expand Down Expand Up @@ -44,3 +50,37 @@ describe('computeHasMetadataOperator', () => {
).toBe(true);
});
});

describe('parseEsqlQuery', () => {
it('returns isMissingMetadataOperator true when query is not aggregating and does not have metadata operator', () => {
computeIsESQLQueryAggregatingMock.mockReturnValueOnce(false);

expect(parseEsqlQuery('from test*')).toEqual({
isEsqlQueryAggregating: false,
isMissingMetadataOperator: true,
});
});

it('returns isMissingMetadataOperator false when query is not aggregating and has metadata operator', () => {
computeIsESQLQueryAggregatingMock.mockReturnValueOnce(false);

expect(parseEsqlQuery('from test* metadata _id')).toEqual({
isEsqlQueryAggregating: false,
isMissingMetadataOperator: false,
});
});

it('returns isMissingMetadataOperator false when query is aggregating', () => {
computeIsESQLQueryAggregatingMock.mockReturnValue(true);

expect(parseEsqlQuery('from test*')).toEqual({
isEsqlQueryAggregating: true,
isMissingMetadataOperator: false,
});

expect(parseEsqlQuery('from test* metadata _id')).toEqual({
isEsqlQueryAggregating: true,
isMissingMetadataOperator: false,
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@
*/

import { isEmpty } from 'lodash';

import type { QueryClient } from '@tanstack/react-query';
import { computeIsESQLQueryAggregating } from '@kbn/securitysolution-utils';

import { KibanaServices } from '../../../common/lib/kibana';
import { securitySolutionQueryClient } from '../../../common/containers/query_client/query_client_provider';

import type { ValidationError, ValidationFunc } from '../../../shared_imports';
import { isEsqlRule } from '../../../../common/detection_engine/utils';
Expand Down Expand Up @@ -48,7 +47,7 @@ export const computeHasMetadataOperator = (esqlQuery: string) => {
export const esqlValidator = async (
...args: Parameters<ValidationFunc>
): Promise<ValidationError<ERROR_CODES> | void | undefined> => {
const [{ value, formData }] = args;
const [{ value, formData, customData }] = args;
const { query: queryValue } = value as FieldValueQueryBar;
const query = queryValue.query as string;
const { ruleType } = formData as DefineStepRule;
Expand All @@ -59,19 +58,19 @@ export const esqlValidator = async (
}

try {
const services = KibanaServices.get();
const queryClient = (customData.value as { queryClient: QueryClient | undefined })?.queryClient;

const isEsqlQueryAggregating = computeIsESQLQueryAggregating(query);
const services = KibanaServices.get();
const { isEsqlQueryAggregating, isMissingMetadataOperator } = parseEsqlQuery(query);

// non-aggregating query which does not have metadata, is not a valid one
if (!isEsqlQueryAggregating && !computeHasMetadataOperator(query)) {
if (isMissingMetadataOperator) {
return {
code: ERROR_CODES.ERR_MISSING_ID_FIELD_FROM_RESULT,
message: i18n.ESQL_VALIDATION_MISSING_ID_IN_QUERY_ERROR,
};
}

const columns = await securitySolutionQueryClient.fetchQuery(
const columns = await queryClient?.fetchQuery(
getEsqlQueryConfig({ esqlQuery: query, search: services.data.search.search })
);

Expand All @@ -92,3 +91,17 @@ export const esqlValidator = async (
return constructValidationError(error);
}
};

/**
* check if esql query valid for Security rule:
* - if it's non aggregation query it must have metadata operator
*/
export const parseEsqlQuery = (query: string) => {
const isEsqlQueryAggregating = computeIsESQLQueryAggregating(query);

return {
isEsqlQueryAggregating,
// non-aggregating query which does not have [metadata], is not a valid one
isMissingMetadataOperator: !isEsqlQueryAggregating && !computeHasMetadataOperator(query),
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,7 @@ describe('description_step', () => {
});

describe('alert suppression', () => {
const ruleTypesWithoutSuppression: Type[] = ['esql', 'machine_learning'];
const ruleTypesWithoutSuppression: Type[] = ['machine_learning'];
vitaliidm marked this conversation as resolved.
Show resolved Hide resolved
const suppressionFields = {
groupByDuration: {
unit: 'm',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { useKibana } from '../../../../common/lib/kibana';
import { useRuleIndices } from '../../../rule_management/logic/use_rule_indices';
import { EsqlAutocomplete } from '../esql_autocomplete';
import { MultiSelectFieldsAutocomplete } from '../multi_select_fields';
import { useInvestigationFields } from '../../hooks/use_investigation_fields';
import { useAllEsqlRuleFields } from '../../hooks';
import { MaxSignals } from '../max_signals';

const CommonUseField = getUseField({ component: Field });
Expand Down Expand Up @@ -133,10 +133,11 @@ const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({
[getFields]
);

const { investigationFields, isLoading: isInvestigationFieldsLoading } = useInvestigationFields({
esqlQuery: isEsqlRuleValue ? esqlQuery : undefined,
indexPatternsFields: indexPattern.fields,
});
const { fields: investigationFields, isLoading: isInvestigationFieldsLoading } =
useAllEsqlRuleFields({
esqlQuery: isEsqlRuleValue ? esqlQuery : undefined,
indexPatternsFields: indexPattern.fields,
});

return (
<>
Expand Down