From 6a173ba19d6b29d2097bb266af5f72e349636f85 Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Mon, 5 Oct 2020 14:39:40 -0600 Subject: [PATCH] [Security Solution][Detections] Adds sequence callout in the exceptions modals for eql rule types (#79007) --- .../common/detection_engine/utils.test.ts | 39 +++++++++- .../common/detection_engine/utils.ts | 8 +++ .../add_exception_modal/index.test.tsx | 72 +++++++++++++++++++ .../exceptions/add_exception_modal/index.tsx | 18 +++++ .../add_exception_modal/translations.ts | 8 +++ .../edit_exception_modal/index.test.tsx | 65 ++++++++++++++++- .../exceptions/edit_exception_modal/index.tsx | 17 +++++ .../edit_exception_modal/translations.ts | 8 +++ 8 files changed, 233 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts index ea50acc9b46be..202733574b69f 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { hasLargeValueList, hasNestedEntry, isThreatMatchRule } from './utils'; +import { hasEqlSequenceQuery, hasLargeValueList, hasNestedEntry, isThreatMatchRule } from './utils'; import { EntriesArray } from '../shared_imports'; describe('#hasLargeValueList', () => { @@ -113,3 +113,40 @@ describe('#hasNestedEntry', () => { }); }); }); + +describe('#hasEqlSequenceQuery', () => { + describe('when a non-sequence query is passed', () => { + const query = 'process where process.name == "regsvr32.exe"'; + it('should return false', () => { + expect(hasEqlSequenceQuery(query)).toEqual(false); + }); + }); + + describe('when a sequence query is passed', () => { + const query = 'sequence [process where process.name = "test.exe"]'; + it('should return true', () => { + expect(hasEqlSequenceQuery(query)).toEqual(true); + }); + }); + + describe('when a sequence query is passed with extra white space and escape characters', () => { + const query = '\tsequence \n [process where process.name = "test.exe"]'; + it('should return true', () => { + expect(hasEqlSequenceQuery(query)).toEqual(true); + }); + }); + + describe('when a non-sequence query is passed using the word sequence', () => { + const query = 'sequence where true'; + it('should return false', () => { + expect(hasEqlSequenceQuery(query)).toEqual(false); + }); + }); + + describe('when a non-sequence query is passed using the word sequence with extra white space and escape characters', () => { + const query = ' sequence\nwhere\ttrue'; + it('should return false', () => { + expect(hasEqlSequenceQuery(query)).toEqual(false); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts index d7b23755699f5..d35c5980d96a2 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts @@ -17,6 +17,14 @@ export const hasNestedEntry = (entries: EntriesArray): boolean => { return found.length > 0; }; +export const hasEqlSequenceQuery = (ruleQuery: string | undefined): boolean => { + if (ruleQuery != null) { + const parsedQuery = ruleQuery.trim().split(/[ \t\r\n]+/); + return parsedQuery[0] === 'sequence' && parsedQuery[1] !== 'where'; + } + return false; +}; + export const isEqlRule = (ruleType: Type | undefined): boolean => ruleType === 'eql'; export const isThresholdRule = (ruleType: Type | undefined): boolean => ruleType === 'threshold'; export const isQueryRule = (ruleType: Type | undefined): boolean => diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx index 037462839c72d..35bd5ee572160 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx @@ -25,6 +25,11 @@ import * as helpers from '../helpers'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { EntriesArray } from '../../../../../../lists/common/schemas/types'; import { ExceptionListItemSchema } from '../../../../../../lists/common'; +import { + getRulesEqlSchemaMock, + getRulesSchemaMock, +} from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; +import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async'; jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_index'); jest.mock('../../../../common/lib/kibana'); @@ -34,6 +39,7 @@ jest.mock('../use_add_exception'); jest.mock('../use_fetch_or_create_rule_exception_list'); jest.mock('../builder'); jest.mock('../../../../shared_imports'); +jest.mock('../../../../detections/containers/detection_engine/rules/use_rule_async'); describe('When the add exception modal is opened', () => { const ruleName = 'test rule'; @@ -73,6 +79,9 @@ describe('When the add exception modal is opened', () => { }, ]); (useCurrentUser as jest.Mock).mockReturnValue({ username: 'test-username' }); + (useRuleAsync as jest.Mock).mockImplementation(() => ({ + rule: getRulesSchemaMock(), + })); }); afterEach(() => { @@ -193,6 +202,9 @@ describe('When the add exception modal is opened', () => { it('should contain the endpoint specific documentation text', () => { expect(wrapper.find('[data-test-subj="add-exception-endpoint-text"]').exists()).toBeTruthy(); }); + it('should not display the eql sequence callout', () => { + expect(wrapper.find('[data-test-subj="eql-sequence-callout"]').exists()).not.toBeTruthy(); + }); }); describe('when there is alert data passed to a detection list exception', () => { @@ -241,6 +253,66 @@ describe('When the add exception modal is opened', () => { .getDOMNode() ).toBeDisabled(); }); + it('should not display the eql sequence callout', () => { + expect(wrapper.find('[data-test-subj="eql-sequence-callout"]').exists()).not.toBeTruthy(); + }); + }); + + describe('when there is an exception being created on a sequence eql rule type', () => { + let wrapper: ReactWrapper; + beforeEach(async () => { + const alertDataMock: Ecs = { _id: 'test-id', file: { path: ['test/path'] } }; + (useRuleAsync as jest.Mock).mockImplementation(() => ({ + rule: { + ...getRulesEqlSchemaMock(), + query: + 'sequence [process where process.name = "test.exe"] [process where process.name = "explorer.exe"]', + }, + })); + wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + const callProps = ExceptionBuilderComponent.mock.calls[0][0]; + await waitFor(() => + callProps.onChange({ exceptionItems: [getExceptionListItemSchemaMock()] }) + ); + }); + it('has the add exception button enabled', () => { + expect( + wrapper.find('button[data-test-subj="add-exception-confirm-button"]').getDOMNode() + ).not.toBeDisabled(); + }); + it('should render the exception builder', () => { + expect(wrapper.find('[data-test-subj="alert-exception-builder"]').exists()).toBeTruthy(); + }); + it('should not prepopulate endpoint items', () => { + expect(defaultEndpointItems).not.toHaveBeenCalled(); + }); + it('should render the close on add exception checkbox', () => { + expect( + wrapper.find('[data-test-subj="close-alert-on-add-add-exception-checkbox"]').exists() + ).toBeTruthy(); + }); + it('should have the bulk close checkbox disabled', () => { + expect( + wrapper + .find('input[data-test-subj="bulk-close-alert-on-add-add-exception-checkbox"]') + .getDOMNode() + ).toBeDisabled(); + }); + it('should display the eql sequence callout', () => { + expect(wrapper.find('[data-test-subj="eql-sequence-callout"]').exists()).toBeTruthy(); + }); }); describe('when there is bulk-closeable alert data passed to an endpoint list exception', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index ad5bc98243467..bf483387580ce 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -19,7 +19,9 @@ import { EuiSpacer, EuiFormRow, EuiText, + EuiCallOut, } from '@elastic/eui'; +import { hasEqlSequenceQuery, isEqlRule } from '../../../../../common/detection_engine/utils'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; import { ExceptionListItemSchema, @@ -315,6 +317,13 @@ export const AddExceptionModal = memo(function AddExceptionModal({ const addExceptionMessage = exceptionListType === 'endpoint' ? i18n.ADD_ENDPOINT_EXCEPTION : i18n.ADD_EXCEPTION; + const isRuleEQLSequenceStatement = useMemo((): boolean => { + if (maybeRule != null) { + return isEqlRule(maybeRule.type) && hasEqlSequenceQuery(maybeRule.query); + } + return false; + }, [maybeRule]); + return ( @@ -353,6 +362,15 @@ export const AddExceptionModal = memo(function AddExceptionModal({ ruleExceptionList && ( <> + {isRuleEQLSequenceStatement && ( + <> + + + + )} {i18n.EXCEPTION_BUILDER_INFO} { const ruleName = 'test rule'; @@ -58,6 +64,9 @@ describe('When the edit exception modal is opened', () => { }, ]); (useCurrentUser as jest.Mock).mockReturnValue({ username: 'test-username' }); + (useRuleAsync as jest.Mock).mockImplementation(() => ({ + rule: getRulesSchemaMock(), + })); }); afterEach(() => { @@ -190,7 +199,58 @@ describe('When the edit exception modal is opened', () => { }); }); - describe('when an detection exception with entries is passed', () => { + describe('when an exception assigned to a sequence eql rule type is passed', () => { + let wrapper: ReactWrapper; + beforeEach(async () => { + (useRuleAsync as jest.Mock).mockImplementation(() => ({ + rule: { + ...getRulesEqlSchemaMock(), + query: + 'sequence [process where process.name = "test.exe"] [process where process.name = "explorer.exe"]', + }, + })); + wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + const callProps = ExceptionBuilderComponent.mock.calls[0][0]; + await waitFor(() => { + callProps.onChange({ exceptionItems: [...callProps.exceptionListItems] }); + }); + }); + it('has the edit exception button enabled', () => { + expect( + wrapper.find('button[data-test-subj="edit-exception-confirm-button"]').getDOMNode() + ).not.toBeDisabled(); + }); + it('renders the exceptions builder', () => { + expect(wrapper.find('[data-test-subj="edit-exception-modal-builder"]').exists()).toBeTruthy(); + }); + it('should not contain the endpoint specific documentation text', () => { + expect(wrapper.find('[data-test-subj="edit-exception-endpoint-text"]').exists()).toBeFalsy(); + }); + it('should have the bulk close checkbox disabled', () => { + expect( + wrapper + .find('input[data-test-subj="close-alert-on-add-edit-exception-checkbox"]') + .getDOMNode() + ).toBeDisabled(); + }); + it('should display the eql sequence callout', () => { + expect(wrapper.find('[data-test-subj="eql-sequence-callout"]').exists()).toBeTruthy(); + }); + }); + + describe('when a detection exception with entries is passed', () => { let wrapper: ReactWrapper; beforeEach(async () => { wrapper = mount( @@ -229,6 +289,9 @@ describe('When the edit exception modal is opened', () => { .getDOMNode() ).toBeDisabled(); }); + it('should not display the eql sequence callout', () => { + expect(wrapper.find('[data-test-subj="eql-sequence-callout"]').exists()).not.toBeTruthy(); + }); }); describe('when an exception with no entries is passed', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index 08f7e3af90d0c..257c8e8c4d873 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -22,6 +22,7 @@ import { EuiCallOut, } from '@elastic/eui'; +import { hasEqlSequenceQuery, isEqlRule } from '../../../../../common/detection_engine/utils'; import { useFetchIndex } from '../../../containers/source'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async'; @@ -246,6 +247,13 @@ export const EditExceptionModal = memo(function EditExceptionModal({ signalIndexName, ]); + const isRuleEQLSequenceStatement = useMemo((): boolean => { + if (maybeRule != null) { + return isEqlRule(maybeRule.type) && hasEqlSequenceQuery(maybeRule.query); + } + return false; + }, [maybeRule]); + return ( @@ -265,6 +273,15 @@ export const EditExceptionModal = memo(function EditExceptionModal({ {!isSignalIndexLoading && !addExceptionIsLoading && !isIndexPatternLoading && ( <> + {isRuleEQLSequenceStatement && ( + <> + + + + )} {i18n.EXCEPTION_BUILDER_INFO}