Skip to content

Commit d47c70c

Browse files
[Security Solution][Exceptions] Implement exceptions for ML rules (#84006)
* Implement exceptions for ML rules * Remove unused import * Better implicit types * Retrieve ML rule index pattern for exception field suggestions and autocomplete * Add ML job logic to edit exception modal * Remove unnecessary logic change Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
1 parent 4f3d72b commit d47c70c

17 files changed

Lines changed: 552 additions & 179 deletions

File tree

x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import { getQueryFilter, buildExceptionFilter, buildEqlSearchRequest } from './get_query_filter';
88
import { Filter, EsQueryConfig } from 'src/plugins/data/public';
99
import { getExceptionListItemSchemaMock } from '../../../lists/common/schemas/response/exception_list_item_schema.mock';
10+
import { ExceptionListItemSchema } from '../shared_imports';
1011

1112
describe('get_filter', () => {
1213
describe('getQueryFilter', () => {
@@ -919,19 +920,27 @@ describe('get_filter', () => {
919920
dateFormatTZ: 'Zulu',
920921
};
921922
test('it should build a filter without chunking exception items', () => {
922-
const exceptionFilter = buildExceptionFilter(
923-
[
924-
{ language: 'kuery', query: 'host.name: linux and some.field: value' },
925-
{ language: 'kuery', query: 'user.name: name' },
923+
const exceptionItem1: ExceptionListItemSchema = {
924+
...getExceptionListItemSchemaMock(),
925+
entries: [
926+
{ field: 'host.name', operator: 'included', type: 'match', value: 'linux' },
927+
{ field: 'some.field', operator: 'included', type: 'match', value: 'value' },
926928
],
927-
{
929+
};
930+
const exceptionItem2: ExceptionListItemSchema = {
931+
...getExceptionListItemSchemaMock(),
932+
entries: [{ field: 'user.name', operator: 'included', type: 'match', value: 'name' }],
933+
};
934+
const exceptionFilter = buildExceptionFilter({
935+
lists: [exceptionItem1, exceptionItem2],
936+
config,
937+
excludeExceptions: true,
938+
chunkSize: 2,
939+
indexPattern: {
928940
fields: [],
929941
title: 'auditbeat-*',
930942
},
931-
config,
932-
true,
933-
2
934-
);
943+
});
935944
expect(exceptionFilter).toEqual({
936945
meta: {
937946
alias: null,
@@ -949,7 +958,7 @@ describe('get_filter', () => {
949958
minimum_should_match: 1,
950959
should: [
951960
{
952-
match: {
961+
match_phrase: {
953962
'host.name': 'linux',
954963
},
955964
},
@@ -961,7 +970,7 @@ describe('get_filter', () => {
961970
minimum_should_match: 1,
962971
should: [
963972
{
964-
match: {
973+
match_phrase: {
965974
'some.field': 'value',
966975
},
967976
},
@@ -976,7 +985,7 @@ describe('get_filter', () => {
976985
minimum_should_match: 1,
977986
should: [
978987
{
979-
match: {
988+
match_phrase: {
980989
'user.name': 'name',
981990
},
982991
},
@@ -990,20 +999,31 @@ describe('get_filter', () => {
990999
});
9911000

9921001
test('it should properly chunk exception items', () => {
993-
const exceptionFilter = buildExceptionFilter(
994-
[
995-
{ language: 'kuery', query: 'host.name: linux and some.field: value' },
996-
{ language: 'kuery', query: 'user.name: name' },
997-
{ language: 'kuery', query: 'file.path: /safe/path' },
1002+
const exceptionItem1: ExceptionListItemSchema = {
1003+
...getExceptionListItemSchemaMock(),
1004+
entries: [
1005+
{ field: 'host.name', operator: 'included', type: 'match', value: 'linux' },
1006+
{ field: 'some.field', operator: 'included', type: 'match', value: 'value' },
9981007
],
999-
{
1008+
};
1009+
const exceptionItem2: ExceptionListItemSchema = {
1010+
...getExceptionListItemSchemaMock(),
1011+
entries: [{ field: 'user.name', operator: 'included', type: 'match', value: 'name' }],
1012+
};
1013+
const exceptionItem3: ExceptionListItemSchema = {
1014+
...getExceptionListItemSchemaMock(),
1015+
entries: [{ field: 'file.path', operator: 'included', type: 'match', value: '/safe/path' }],
1016+
};
1017+
const exceptionFilter = buildExceptionFilter({
1018+
lists: [exceptionItem1, exceptionItem2, exceptionItem3],
1019+
config,
1020+
excludeExceptions: true,
1021+
chunkSize: 2,
1022+
indexPattern: {
10001023
fields: [],
10011024
title: 'auditbeat-*',
10021025
},
1003-
config,
1004-
true,
1005-
2
1006-
);
1026+
});
10071027
expect(exceptionFilter).toEqual({
10081028
meta: {
10091029
alias: null,
@@ -1024,7 +1044,7 @@ describe('get_filter', () => {
10241044
minimum_should_match: 1,
10251045
should: [
10261046
{
1027-
match: {
1047+
match_phrase: {
10281048
'host.name': 'linux',
10291049
},
10301050
},
@@ -1036,7 +1056,7 @@ describe('get_filter', () => {
10361056
minimum_should_match: 1,
10371057
should: [
10381058
{
1039-
match: {
1059+
match_phrase: {
10401060
'some.field': 'value',
10411061
},
10421062
},
@@ -1051,7 +1071,7 @@ describe('get_filter', () => {
10511071
minimum_should_match: 1,
10521072
should: [
10531073
{
1054-
match: {
1074+
match_phrase: {
10551075
'user.name': 'name',
10561076
},
10571077
},
@@ -1069,7 +1089,7 @@ describe('get_filter', () => {
10691089
minimum_should_match: 1,
10701090
should: [
10711091
{
1072-
match: {
1092+
match_phrase: {
10731093
'file.path': '/safe/path',
10741094
},
10751095
},

x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts

Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
import {
88
Filter,
9-
Query,
109
IIndexPattern,
1110
isFilterDisabled,
1211
buildEsQuery,
@@ -18,15 +17,10 @@ import {
1817
} from '../../../lists/common/schemas';
1918
import { ESBoolQuery } from '../typed_json';
2019
import { buildExceptionListQueries } from './build_exceptions_query';
21-
import {
22-
Query as QueryString,
23-
Language,
24-
Index,
25-
TimestampOverrideOrUndefined,
26-
} from './schemas/common/schemas';
20+
import { Query, Language, Index, TimestampOverrideOrUndefined } from './schemas/common/schemas';
2721

2822
export const getQueryFilter = (
29-
query: QueryString,
23+
query: Query,
3024
language: Language,
3125
filters: Array<Partial<Filter>>,
3226
index: Index,
@@ -53,19 +47,18 @@ export const getQueryFilter = (
5347
* buildEsQuery, this allows us to offer nested queries
5448
* regardless
5549
*/
56-
const exceptionQueries = buildExceptionListQueries({ language: 'kuery', lists });
57-
if (exceptionQueries.length > 0) {
58-
// Assume that `indices.query.bool.max_clause_count` is at least 1024 (the default value),
59-
// allowing us to make 1024-item chunks of exception list items.
60-
// Discussion at https://issues.apache.org/jira/browse/LUCENE-4835 indicates that 1024 is a
61-
// very conservative value.
62-
const exceptionFilter = buildExceptionFilter(
63-
exceptionQueries,
64-
indexPattern,
65-
config,
66-
excludeExceptions,
67-
1024
68-
);
50+
// Assume that `indices.query.bool.max_clause_count` is at least 1024 (the default value),
51+
// allowing us to make 1024-item chunks of exception list items.
52+
// Discussion at https://issues.apache.org/jira/browse/LUCENE-4835 indicates that 1024 is a
53+
// very conservative value.
54+
const exceptionFilter = buildExceptionFilter({
55+
lists,
56+
config,
57+
excludeExceptions,
58+
chunkSize: 1024,
59+
indexPattern,
60+
});
61+
if (exceptionFilter !== undefined) {
6962
enabledFilters.push(exceptionFilter);
7063
}
7164
const initialQuery = { query, language };
@@ -101,15 +94,17 @@ export const buildEqlSearchRequest = (
10194
ignoreFilterIfFieldNotInIndex: false,
10295
dateFormatTZ: 'Zulu',
10396
};
104-
const exceptionQueries = buildExceptionListQueries({ language: 'kuery', lists: exceptionLists });
105-
let exceptionFilter: Filter | undefined;
106-
if (exceptionQueries.length > 0) {
107-
// Assume that `indices.query.bool.max_clause_count` is at least 1024 (the default value),
108-
// allowing us to make 1024-item chunks of exception list items.
109-
// Discussion at https://issues.apache.org/jira/browse/LUCENE-4835 indicates that 1024 is a
110-
// very conservative value.
111-
exceptionFilter = buildExceptionFilter(exceptionQueries, indexPattern, config, true, 1024);
112-
}
97+
// Assume that `indices.query.bool.max_clause_count` is at least 1024 (the default value),
98+
// allowing us to make 1024-item chunks of exception list items.
99+
// Discussion at https://issues.apache.org/jira/browse/LUCENE-4835 indicates that 1024 is a
100+
// very conservative value.
101+
const exceptionFilter = buildExceptionFilter({
102+
lists: exceptionLists,
103+
config,
104+
excludeExceptions: true,
105+
chunkSize: 1024,
106+
indexPattern,
107+
});
113108
const indexString = index.join();
114109
const requestFilter: unknown[] = [
115110
{
@@ -154,13 +149,23 @@ export const buildEqlSearchRequest = (
154149
}
155150
};
156151

157-
export const buildExceptionFilter = (
158-
exceptionQueries: Query[],
159-
indexPattern: IIndexPattern,
160-
config: EsQueryConfig,
161-
excludeExceptions: boolean,
162-
chunkSize: number
163-
) => {
152+
export const buildExceptionFilter = ({
153+
lists,
154+
config,
155+
excludeExceptions,
156+
chunkSize,
157+
indexPattern,
158+
}: {
159+
lists: Array<ExceptionListItemSchema | CreateExceptionListItemSchema>;
160+
config: EsQueryConfig;
161+
excludeExceptions: boolean;
162+
chunkSize: number;
163+
indexPattern?: IIndexPattern;
164+
}) => {
165+
const exceptionQueries = buildExceptionListQueries({ language: 'kuery', lists });
166+
if (exceptionQueries.length === 0) {
167+
return undefined;
168+
}
164169
const exceptionFilter: Filter = {
165170
meta: {
166171
alias: null,

x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7+
/* eslint complexity: ["error", 30]*/
8+
79
import React, { memo, useEffect, useState, useCallback, useMemo } from 'react';
810
import styled, { css } from 'styled-components';
911
import {
@@ -53,6 +55,7 @@ import {
5355
import { ErrorInfo, ErrorCallout } from '../error_callout';
5456
import { ExceptionsBuilderExceptionItem } from '../types';
5557
import { useFetchIndex } from '../../../containers/source';
58+
import { useGetInstalledJob } from '../../ml/hooks/use_get_jobs';
5659

5760
export interface AddExceptionModalProps {
5861
ruleName: string;
@@ -108,7 +111,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({
108111
const { http } = useKibana().services;
109112
const [errorsExist, setErrorExists] = useState(false);
110113
const [comment, setComment] = useState('');
111-
const { rule: maybeRule } = useRuleAsync(ruleId);
114+
const { rule: maybeRule, loading: isRuleLoading } = useRuleAsync(ruleId);
112115
const [shouldCloseAlert, setShouldCloseAlert] = useState(false);
113116
const [shouldBulkCloseAlert, setShouldBulkCloseAlert] = useState(false);
114117
const [shouldDisableBulkClose, setShouldDisableBulkClose] = useState(false);
@@ -124,8 +127,22 @@ export const AddExceptionModal = memo(function AddExceptionModal({
124127
const [isSignalIndexPatternLoading, { indexPatterns: signalIndexPatterns }] = useFetchIndex(
125128
memoSignalIndexName
126129
);
127-
const [isIndexPatternLoading, { indexPatterns }] = useFetchIndex(ruleIndices);
128130

131+
const memoMlJobIds = useMemo(
132+
() => (maybeRule?.machine_learning_job_id != null ? [maybeRule.machine_learning_job_id] : []),
133+
[maybeRule]
134+
);
135+
const { loading: mlJobLoading, jobs } = useGetInstalledJob(memoMlJobIds);
136+
137+
const memoRuleIndices = useMemo(() => {
138+
if (jobs.length > 0) {
139+
return jobs[0].results_index_name ? [`.ml-anomalies-${jobs[0].results_index_name}`] : [];
140+
} else {
141+
return ruleIndices;
142+
}
143+
}, [jobs, ruleIndices]);
144+
145+
const [isIndexPatternLoading, { indexPatterns }] = useFetchIndex(memoRuleIndices);
129146
const onError = useCallback(
130147
(error: Error): void => {
131148
addError(error, { title: i18n.ADD_EXCEPTION_ERROR });
@@ -364,6 +381,8 @@ export const AddExceptionModal = memo(function AddExceptionModal({
364381
!isSignalIndexPatternLoading &&
365382
!isLoadingExceptionList &&
366383
!isIndexPatternLoading &&
384+
!isRuleLoading &&
385+
!mlJobLoading &&
367386
ruleExceptionList && (
368387
<>
369388
<ModalBodySection className="builder-section">

0 commit comments

Comments
 (0)