Skip to content

Commit ceb8cdd

Browse files
authored
[Security Solution][Detections] Adds sequence callout in the exceptions modals for eql rule types (#79007) (#79562)
1 parent 3029dd2 commit ceb8cdd

File tree

8 files changed

+233
-2
lines changed

8 files changed

+233
-2
lines changed

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

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import { hasLargeValueList, hasNestedEntry, isThreatMatchRule } from './utils';
7+
import { hasEqlSequenceQuery, hasLargeValueList, hasNestedEntry, isThreatMatchRule } from './utils';
88
import { EntriesArray } from '../shared_imports';
99

1010
describe('#hasLargeValueList', () => {
@@ -113,3 +113,40 @@ describe('#hasNestedEntry', () => {
113113
});
114114
});
115115
});
116+
117+
describe('#hasEqlSequenceQuery', () => {
118+
describe('when a non-sequence query is passed', () => {
119+
const query = 'process where process.name == "regsvr32.exe"';
120+
it('should return false', () => {
121+
expect(hasEqlSequenceQuery(query)).toEqual(false);
122+
});
123+
});
124+
125+
describe('when a sequence query is passed', () => {
126+
const query = 'sequence [process where process.name = "test.exe"]';
127+
it('should return true', () => {
128+
expect(hasEqlSequenceQuery(query)).toEqual(true);
129+
});
130+
});
131+
132+
describe('when a sequence query is passed with extra white space and escape characters', () => {
133+
const query = '\tsequence \n [process where process.name = "test.exe"]';
134+
it('should return true', () => {
135+
expect(hasEqlSequenceQuery(query)).toEqual(true);
136+
});
137+
});
138+
139+
describe('when a non-sequence query is passed using the word sequence', () => {
140+
const query = 'sequence where true';
141+
it('should return false', () => {
142+
expect(hasEqlSequenceQuery(query)).toEqual(false);
143+
});
144+
});
145+
146+
describe('when a non-sequence query is passed using the word sequence with extra white space and escape characters', () => {
147+
const query = ' sequence\nwhere\ttrue';
148+
it('should return false', () => {
149+
expect(hasEqlSequenceQuery(query)).toEqual(false);
150+
});
151+
});
152+
});

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ export const hasNestedEntry = (entries: EntriesArray): boolean => {
1717
return found.length > 0;
1818
};
1919

20+
export const hasEqlSequenceQuery = (ruleQuery: string | undefined): boolean => {
21+
if (ruleQuery != null) {
22+
const parsedQuery = ruleQuery.trim().split(/[ \t\r\n]+/);
23+
return parsedQuery[0] === 'sequence' && parsedQuery[1] !== 'where';
24+
}
25+
return false;
26+
};
27+
2028
export const isEqlRule = (ruleType: Type | undefined): boolean => ruleType === 'eql';
2129
export const isThresholdRule = (ruleType: Type | undefined): boolean => ruleType === 'threshold';
2230
export const isQueryRule = (ruleType: Type | undefined): boolean =>

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

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ import * as helpers from '../helpers';
2525
import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
2626
import { EntriesArray } from '../../../../../../lists/common/schemas/types';
2727
import { ExceptionListItemSchema } from '../../../../../../lists/common';
28+
import {
29+
getRulesEqlSchemaMock,
30+
getRulesSchemaMock,
31+
} from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks';
32+
import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async';
2833

2934
jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_index');
3035
jest.mock('../../../../common/lib/kibana');
@@ -34,6 +39,7 @@ jest.mock('../use_add_exception');
3439
jest.mock('../use_fetch_or_create_rule_exception_list');
3540
jest.mock('../builder');
3641
jest.mock('../../../../shared_imports');
42+
jest.mock('../../../../detections/containers/detection_engine/rules/use_rule_async');
3743

3844
describe('When the add exception modal is opened', () => {
3945
const ruleName = 'test rule';
@@ -73,6 +79,9 @@ describe('When the add exception modal is opened', () => {
7379
},
7480
]);
7581
(useCurrentUser as jest.Mock).mockReturnValue({ username: 'test-username' });
82+
(useRuleAsync as jest.Mock).mockImplementation(() => ({
83+
rule: getRulesSchemaMock(),
84+
}));
7685
});
7786

7887
afterEach(() => {
@@ -193,6 +202,9 @@ describe('When the add exception modal is opened', () => {
193202
it('should contain the endpoint specific documentation text', () => {
194203
expect(wrapper.find('[data-test-subj="add-exception-endpoint-text"]').exists()).toBeTruthy();
195204
});
205+
it('should not display the eql sequence callout', () => {
206+
expect(wrapper.find('[data-test-subj="eql-sequence-callout"]').exists()).not.toBeTruthy();
207+
});
196208
});
197209

198210
describe('when there is alert data passed to a detection list exception', () => {
@@ -241,6 +253,66 @@ describe('When the add exception modal is opened', () => {
241253
.getDOMNode()
242254
).toBeDisabled();
243255
});
256+
it('should not display the eql sequence callout', () => {
257+
expect(wrapper.find('[data-test-subj="eql-sequence-callout"]').exists()).not.toBeTruthy();
258+
});
259+
});
260+
261+
describe('when there is an exception being created on a sequence eql rule type', () => {
262+
let wrapper: ReactWrapper;
263+
beforeEach(async () => {
264+
const alertDataMock: Ecs = { _id: 'test-id', file: { path: ['test/path'] } };
265+
(useRuleAsync as jest.Mock).mockImplementation(() => ({
266+
rule: {
267+
...getRulesEqlSchemaMock(),
268+
query:
269+
'sequence [process where process.name = "test.exe"] [process where process.name = "explorer.exe"]',
270+
},
271+
}));
272+
wrapper = mount(
273+
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
274+
<AddExceptionModal
275+
ruleId={'123'}
276+
ruleIndices={['filebeat-*']}
277+
ruleName={ruleName}
278+
exceptionListType={'detection'}
279+
onCancel={jest.fn()}
280+
onConfirm={jest.fn()}
281+
alertData={alertDataMock}
282+
/>
283+
</ThemeProvider>
284+
);
285+
const callProps = ExceptionBuilderComponent.mock.calls[0][0];
286+
await waitFor(() =>
287+
callProps.onChange({ exceptionItems: [getExceptionListItemSchemaMock()] })
288+
);
289+
});
290+
it('has the add exception button enabled', () => {
291+
expect(
292+
wrapper.find('button[data-test-subj="add-exception-confirm-button"]').getDOMNode()
293+
).not.toBeDisabled();
294+
});
295+
it('should render the exception builder', () => {
296+
expect(wrapper.find('[data-test-subj="alert-exception-builder"]').exists()).toBeTruthy();
297+
});
298+
it('should not prepopulate endpoint items', () => {
299+
expect(defaultEndpointItems).not.toHaveBeenCalled();
300+
});
301+
it('should render the close on add exception checkbox', () => {
302+
expect(
303+
wrapper.find('[data-test-subj="close-alert-on-add-add-exception-checkbox"]').exists()
304+
).toBeTruthy();
305+
});
306+
it('should have the bulk close checkbox disabled', () => {
307+
expect(
308+
wrapper
309+
.find('input[data-test-subj="bulk-close-alert-on-add-add-exception-checkbox"]')
310+
.getDOMNode()
311+
).toBeDisabled();
312+
});
313+
it('should display the eql sequence callout', () => {
314+
expect(wrapper.find('[data-test-subj="eql-sequence-callout"]').exists()).toBeTruthy();
315+
});
244316
});
245317

246318
describe('when there is bulk-closeable alert data passed to an endpoint list exception', () => {

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import {
1919
EuiSpacer,
2020
EuiFormRow,
2121
EuiText,
22+
EuiCallOut,
2223
} from '@elastic/eui';
24+
import { hasEqlSequenceQuery, isEqlRule } from '../../../../../common/detection_engine/utils';
2325
import { Status } from '../../../../../common/detection_engine/schemas/common/schemas';
2426
import {
2527
ExceptionListItemSchema,
@@ -315,6 +317,13 @@ export const AddExceptionModal = memo(function AddExceptionModal({
315317
const addExceptionMessage =
316318
exceptionListType === 'endpoint' ? i18n.ADD_ENDPOINT_EXCEPTION : i18n.ADD_EXCEPTION;
317319

320+
const isRuleEQLSequenceStatement = useMemo((): boolean => {
321+
if (maybeRule != null) {
322+
return isEqlRule(maybeRule.type) && hasEqlSequenceQuery(maybeRule.query);
323+
}
324+
return false;
325+
}, [maybeRule]);
326+
318327
return (
319328
<EuiOverlayMask onClick={onCancel}>
320329
<Modal onClose={onCancel} data-test-subj="add-exception-modal">
@@ -353,6 +362,15 @@ export const AddExceptionModal = memo(function AddExceptionModal({
353362
ruleExceptionList && (
354363
<>
355364
<ModalBodySection className="builder-section">
365+
{isRuleEQLSequenceStatement && (
366+
<>
367+
<EuiCallOut
368+
data-test-subj="eql-sequence-callout"
369+
title={i18n.ADD_EXCEPTION_SEQUENCE_WARNING}
370+
/>
371+
<EuiSpacer />
372+
</>
373+
)}
356374
<EuiText>{i18n.EXCEPTION_BUILDER_INFO}</EuiText>
357375
<EuiSpacer />
358376
<ExceptionBuilderComponent

x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/translations.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,11 @@ export const EXCEPTION_BUILDER_INFO = i18n.translate(
8181
defaultMessage: "Alerts are generated when the rule's conditions are met, except when:",
8282
}
8383
);
84+
85+
export const ADD_EXCEPTION_SEQUENCE_WARNING = i18n.translate(
86+
'xpack.securitySolution.exceptions.addException.sequenceWarning',
87+
{
88+
defaultMessage:
89+
"This rule's query contains an EQL sequence statement. The exception created will apply to all events in the sequence.",
90+
}
91+
);

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

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ import { useSignalIndex } from '../../../../detections/containers/detection_engi
2222
import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
2323
import { EntriesArray } from '../../../../../../lists/common/schemas/types';
2424
import * as builder from '../builder';
25+
import {
26+
getRulesEqlSchemaMock,
27+
getRulesSchemaMock,
28+
} from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks';
29+
import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async';
2530

2631
jest.mock('../../../../common/lib/kibana');
2732
jest.mock('../../../../detections/containers/detection_engine/rules');
@@ -30,6 +35,7 @@ jest.mock('../../../containers/source');
3035
jest.mock('../use_fetch_or_create_rule_exception_list');
3136
jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_index');
3237
jest.mock('../builder');
38+
jest.mock('../../../../detections/containers/detection_engine/rules/use_rule_async');
3339

3440
describe('When the edit exception modal is opened', () => {
3541
const ruleName = 'test rule';
@@ -58,6 +64,9 @@ describe('When the edit exception modal is opened', () => {
5864
},
5965
]);
6066
(useCurrentUser as jest.Mock).mockReturnValue({ username: 'test-username' });
67+
(useRuleAsync as jest.Mock).mockImplementation(() => ({
68+
rule: getRulesSchemaMock(),
69+
}));
6170
});
6271

6372
afterEach(() => {
@@ -190,7 +199,58 @@ describe('When the edit exception modal is opened', () => {
190199
});
191200
});
192201

193-
describe('when an detection exception with entries is passed', () => {
202+
describe('when an exception assigned to a sequence eql rule type is passed', () => {
203+
let wrapper: ReactWrapper;
204+
beforeEach(async () => {
205+
(useRuleAsync as jest.Mock).mockImplementation(() => ({
206+
rule: {
207+
...getRulesEqlSchemaMock(),
208+
query:
209+
'sequence [process where process.name = "test.exe"] [process where process.name = "explorer.exe"]',
210+
},
211+
}));
212+
wrapper = mount(
213+
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
214+
<EditExceptionModal
215+
ruleIndices={['filebeat-*']}
216+
ruleId="123"
217+
ruleName={ruleName}
218+
exceptionListType={'detection'}
219+
onCancel={jest.fn()}
220+
onConfirm={jest.fn()}
221+
exceptionItem={getExceptionListItemSchemaMock()}
222+
/>
223+
</ThemeProvider>
224+
);
225+
const callProps = ExceptionBuilderComponent.mock.calls[0][0];
226+
await waitFor(() => {
227+
callProps.onChange({ exceptionItems: [...callProps.exceptionListItems] });
228+
});
229+
});
230+
it('has the edit exception button enabled', () => {
231+
expect(
232+
wrapper.find('button[data-test-subj="edit-exception-confirm-button"]').getDOMNode()
233+
).not.toBeDisabled();
234+
});
235+
it('renders the exceptions builder', () => {
236+
expect(wrapper.find('[data-test-subj="edit-exception-modal-builder"]').exists()).toBeTruthy();
237+
});
238+
it('should not contain the endpoint specific documentation text', () => {
239+
expect(wrapper.find('[data-test-subj="edit-exception-endpoint-text"]').exists()).toBeFalsy();
240+
});
241+
it('should have the bulk close checkbox disabled', () => {
242+
expect(
243+
wrapper
244+
.find('input[data-test-subj="close-alert-on-add-edit-exception-checkbox"]')
245+
.getDOMNode()
246+
).toBeDisabled();
247+
});
248+
it('should display the eql sequence callout', () => {
249+
expect(wrapper.find('[data-test-subj="eql-sequence-callout"]').exists()).toBeTruthy();
250+
});
251+
});
252+
253+
describe('when a detection exception with entries is passed', () => {
194254
let wrapper: ReactWrapper;
195255
beforeEach(async () => {
196256
wrapper = mount(
@@ -229,6 +289,9 @@ describe('When the edit exception modal is opened', () => {
229289
.getDOMNode()
230290
).toBeDisabled();
231291
});
292+
it('should not display the eql sequence callout', () => {
293+
expect(wrapper.find('[data-test-subj="eql-sequence-callout"]').exists()).not.toBeTruthy();
294+
});
232295
});
233296

234297
describe('when an exception with no entries is passed', () => {

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
EuiCallOut,
2323
} from '@elastic/eui';
2424

25+
import { hasEqlSequenceQuery, isEqlRule } from '../../../../../common/detection_engine/utils';
2526
import { useFetchIndex } from '../../../containers/source';
2627
import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index';
2728
import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async';
@@ -246,6 +247,13 @@ export const EditExceptionModal = memo(function EditExceptionModal({
246247
signalIndexName,
247248
]);
248249

250+
const isRuleEQLSequenceStatement = useMemo((): boolean => {
251+
if (maybeRule != null) {
252+
return isEqlRule(maybeRule.type) && hasEqlSequenceQuery(maybeRule.query);
253+
}
254+
return false;
255+
}, [maybeRule]);
256+
249257
return (
250258
<EuiOverlayMask onClick={onCancel}>
251259
<Modal onClose={onCancel} data-test-subj="add-exception-modal">
@@ -265,6 +273,15 @@ export const EditExceptionModal = memo(function EditExceptionModal({
265273
{!isSignalIndexLoading && !addExceptionIsLoading && !isIndexPatternLoading && (
266274
<>
267275
<ModalBodySection className="builder-section">
276+
{isRuleEQLSequenceStatement && (
277+
<>
278+
<EuiCallOut
279+
data-test-subj="eql-sequence-callout"
280+
title={i18n.EDIT_EXCEPTION_SEQUENCE_WARNING}
281+
/>
282+
<EuiSpacer />
283+
</>
284+
)}
268285
<EuiText>{i18n.EXCEPTION_BUILDER_INFO}</EuiText>
269286
<EuiSpacer />
270287
<ExceptionBuilderComponent

x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/translations.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,11 @@ export const VERSION_CONFLICT_ERROR_DESCRIPTION = i18n.translate(
8989
"It appears this exception was updated since you first selected to edit it. Try clicking 'Cancel' and editing the exception again.",
9090
}
9191
);
92+
93+
export const EDIT_EXCEPTION_SEQUENCE_WARNING = i18n.translate(
94+
'xpack.securitySolution.exceptions.editException.sequenceWarning',
95+
{
96+
defaultMessage:
97+
"This rule's query contains an EQL sequence statement. The exception modified will apply to all events in the sequence.",
98+
}
99+
);

0 commit comments

Comments
 (0)