diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx index 22d14ec6bedb1..85ff3278ba45d 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx @@ -112,7 +112,7 @@ export const AddExceptionComments = memo(function AddExceptionComments({ { + const ruleName = 'test rule'; + let defaultEndpointItems: jest.SpyInstance>; + let ExceptionBuilderComponent: jest.SpyInstance>; + beforeEach(() => { + defaultEndpointItems = jest.spyOn(helpers, 'defaultEndpointExceptionItems'); + ExceptionBuilderComponent = jest + .spyOn(builder, 'ExceptionBuilderComponent') + .mockReturnValue(<>); + + const kibanaMock = createUseKibanaMock()(); + useKibanaMock.mockImplementation(() => ({ + ...kibanaMock, + })); + (useAddOrUpdateException as jest.Mock).mockImplementation(() => [ + { isLoading: false }, + jest.fn(), + ]); + (useFetchOrCreateRuleExceptionList as jest.Mock).mockImplementation(() => [ + false, + getExceptionListSchemaMock(), + ]); + (useSignalIndex as jest.Mock).mockImplementation(() => ({ + loading: false, + signalIndexName: 'mock-siem-signals-index', + })); + (useFetchIndexPatterns as jest.Mock).mockImplementation(() => [ + { + isLoading: false, + indexPatterns: stubIndexPattern, + }, + ]); + (useCurrentUser as jest.Mock).mockReturnValue({ username: 'test-username' }); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + describe('when the modal is loading', () => { + let wrapper: ReactWrapper; + beforeEach(() => { + // Mocks one of the hooks as loading + (useFetchIndexPatterns as jest.Mock).mockImplementation(() => [ + { + isLoading: true, + indexPatterns: stubIndexPattern, + }, + ]); + wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + }); + it('should show the loading spinner', () => { + expect(wrapper.find('[data-test-subj="loadingAddExceptionModal"]').exists()).toBeTruthy(); + }); + }); + + describe('when there is no alert data passed to an endpoint list exception', () => { + let wrapper: ReactWrapper; + beforeEach(() => { + wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + const callProps = ExceptionBuilderComponent.mock.calls[0][0]; + act(() => callProps.onChange({ exceptionItems: [] })); + }); + it('has the add exception button disabled', () => { + expect( + wrapper.find('button[data-test-subj="add-exception-confirm-button"]').getDOMNode() + ).toBeDisabled(); + }); + it('should render the exception builder', () => { + expect(wrapper.find('[data-test-subj="alert-exception-builder"]').exists()).toBeTruthy(); + }); + it('should not render the close on add exception checkbox', () => { + expect( + wrapper.find('[data-test-subj="close-alert-on-add-add-exception-checkbox"]').exists() + ).toBeFalsy(); + }); + it('should contain the endpoint specific documentation text', () => { + expect(wrapper.find('[data-test-subj="add-exception-endpoint-text"]').exists()).toBeTruthy(); + }); + }); + + describe('when there is alert data passed to an endpoint list exception', () => { + let wrapper: ReactWrapper; + beforeEach(() => { + const alertDataMock: { ecsData: Ecs; nonEcsData: TimelineNonEcsData[] } = { + ecsData: { _id: 'test-id' }, + nonEcsData: [{ field: 'file.path', value: ['test/path'] }], + }; + wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + const callProps = ExceptionBuilderComponent.mock.calls[0][0]; + act(() => callProps.onChange({ exceptionItems: [...callProps.exceptionListItems] })); + }); + 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 prepopulate endpoint items', () => { + expect(defaultEndpointItems).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 contain the endpoint specific documentation text', () => { + expect(wrapper.find('[data-test-subj="add-exception-endpoint-text"]').exists()).toBeTruthy(); + }); + }); + + describe('when there is alert data passed to a detection list exception', () => { + let wrapper: ReactWrapper; + beforeEach(() => { + const alertDataMock: { ecsData: Ecs; nonEcsData: TimelineNonEcsData[] } = { + ecsData: { _id: 'test-id' }, + nonEcsData: [{ field: 'file.path', value: ['test/path'] }], + }; + wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + const callProps = ExceptionBuilderComponent.mock.calls[0][0]; + act(() => 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(); + }); + }); + + describe('when there is bulk-closeable alert data passed to an endpoint list exception', () => { + let wrapper: ReactWrapper; + let callProps: { + onChange: (props: { exceptionItems: ExceptionListItemSchema[] }) => void; + exceptionListItems: ExceptionListItemSchema[]; + }; + beforeEach(() => { + // Mocks the index patterns to contain the pre-populated endpoint fields so that the exception qualifies as bulk closable + (useFetchIndexPatterns as jest.Mock).mockImplementation(() => [ + { + isLoading: false, + indexPatterns: { + ...stubIndexPattern, + fields: [ + { name: 'file.path.text', type: 'string' }, + { name: 'subject_name', type: 'string' }, + { name: 'trusted', type: 'string' }, + { name: 'file.hash.sha256', type: 'string' }, + { name: 'event.code', type: 'string' }, + ], + }, + }, + ]); + const alertDataMock: { ecsData: Ecs; nonEcsData: TimelineNonEcsData[] } = { + ecsData: { _id: 'test-id' }, + nonEcsData: [{ field: 'file.path', value: ['test/path'] }], + }; + wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + callProps = ExceptionBuilderComponent.mock.calls[0][0]; + act(() => callProps.onChange({ exceptionItems: [...callProps.exceptionListItems] })); + }); + 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 prepopulate endpoint items', () => { + expect(defaultEndpointItems).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 contain the endpoint specific documentation text', () => { + expect(wrapper.find('[data-test-subj="add-exception-endpoint-text"]').exists()).toBeTruthy(); + }); + it('should have the bulk close checkbox enabled', () => { + expect( + wrapper + .find('input[data-test-subj="bulk-close-alert-on-add-add-exception-checkbox"]') + .getDOMNode() + ).not.toBeDisabled(); + }); + describe('when a "is in list" entry is added', () => { + it('should have the bulk close checkbox disabled', () => { + act(() => + callProps.onChange({ + exceptionItems: [ + ...callProps.exceptionListItems, + { + ...getExceptionListItemSchemaMock(), + entries: [ + { field: 'event.code', operator: 'included', type: 'list' }, + ] as EntriesArray, + }, + ], + }) + ); + + expect( + wrapper + .find('input[data-test-subj="bulk-close-alert-on-add-add-exception-checkbox"]') + .getDOMNode() + ).toBeDisabled(); + }); + }); + }); +}); 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 7526c52d16fde..03051ead357c9 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 @@ -196,7 +196,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ setShouldDisableBulkClose( entryHasListType(exceptionItemsToAdd) || entryHasNonEcsType(exceptionItemsToAdd, signalIndexPatterns) || - exceptionItemsToAdd.length === 0 + exceptionItemsToAdd.every((item) => item.entries.length === 0) ); } }, [ @@ -344,6 +344,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ {alertData !== undefined && alertStatus !== 'closed' && ( - + {i18n.ENDPOINT_QUARANTINE_TEXT} @@ -380,6 +382,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ {i18n.CANCEL} { + const ruleName = 'test rule'; + + let ExceptionBuilderComponent: jest.SpyInstance>; + + beforeEach(() => { + ExceptionBuilderComponent = jest + .spyOn(builder, 'ExceptionBuilderComponent') + .mockReturnValue(<>); + + const kibanaMock = createUseKibanaMock()(); + useKibanaMock.mockImplementation(() => ({ + ...kibanaMock, + })); + (useSignalIndex as jest.Mock).mockReturnValue({ + loading: false, + signalIndexName: 'test-signal', + }); + (useAddOrUpdateException as jest.Mock).mockImplementation(() => [ + { isLoading: false }, + jest.fn(), + ]); + (useFetchIndexPatterns as jest.Mock).mockImplementation(() => [ + { + isLoading: false, + indexPatterns: stubIndexPatternWithFields, + }, + ]); + (useCurrentUser as jest.Mock).mockReturnValue({ username: 'test-username' }); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + describe('when the modal is loading', () => { + let wrapper: ReactWrapper; + beforeEach(() => { + (useFetchIndexPatterns as jest.Mock).mockImplementation(() => [ + { + isLoading: true, + indexPatterns: stubIndexPattern, + }, + ]); + wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + }); + it('renders the loading spinner', () => { + expect(wrapper.find('[data-test-subj="loadingEditExceptionModal"]').exists()).toBeTruthy(); + }); + }); + + describe('when an endpoint exception with exception data is passed', () => { + describe('when exception entry fields are included in the index pattern', () => { + let wrapper: ReactWrapper; + beforeEach(() => { + const exceptionItemMock = { + ...getExceptionListItemSchemaMock(), + entries: [ + { field: 'response', operator: 'included', type: 'match', value: '3' }, + ] as EntriesArray, + }; + wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + const callProps = ExceptionBuilderComponent.mock.calls[0][0]; + act(() => 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('should have the bulk close checkbox enabled', () => { + expect( + wrapper + .find('input[data-test-subj="close-alert-on-add-edit-exception-checkbox"]') + .getDOMNode() + ).not.toBeDisabled(); + }); + it('renders the exceptions builder', () => { + expect( + wrapper.find('[data-test-subj="edit-exception-modal-builder"]').exists() + ).toBeTruthy(); + }); + it('should contain the endpoint specific documentation text', () => { + expect( + wrapper.find('[data-test-subj="edit-exception-endpoint-text"]').exists() + ).toBeTruthy(); + }); + }); + + describe("when exception entry fields aren't included in the index pattern", () => { + let wrapper: ReactWrapper; + beforeEach(() => { + wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + const callProps = ExceptionBuilderComponent.mock.calls[0][0]; + act(() => 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('should have the bulk close checkbox disabled', () => { + expect( + wrapper + .find('input[data-test-subj="close-alert-on-add-edit-exception-checkbox"]') + .getDOMNode() + ).toBeDisabled(); + }); + it('renders the exceptions builder', () => { + expect( + wrapper.find('[data-test-subj="edit-exception-modal-builder"]').exists() + ).toBeTruthy(); + }); + it('should contain the endpoint specific documentation text', () => { + expect( + wrapper.find('[data-test-subj="edit-exception-endpoint-text"]').exists() + ).toBeTruthy(); + }); + }); + }); + + describe('when an detection exception with entries is passed', () => { + let wrapper: ReactWrapper; + beforeEach(() => { + wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + const callProps = ExceptionBuilderComponent.mock.calls[0][0]; + act(() => 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(); + }); + }); + + describe('when an exception with no entries is passed', () => { + let wrapper: ReactWrapper; + beforeEach(() => { + const exceptionItemMock = { ...getExceptionListItemSchemaMock(), entries: [] }; + wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + const callProps = ExceptionBuilderComponent.mock.calls[0][0]; + act(() => callProps.onChange({ exceptionItems: [...callProps.exceptionListItems] })); + }); + it('has the edit exception button disabled', () => { + expect( + wrapper.find('button[data-test-subj="edit-exception-confirm-button"]').getDOMNode() + ).toBeDisabled(); + }); + it('renders the exceptions builder', () => { + expect(wrapper.find('[data-test-subj="edit-exception-modal-builder"]').exists()).toBeTruthy(); + }); + it('should have the bulk close checkbox disabled', () => { + expect( + wrapper + .find('input[data-test-subj="close-alert-on-add-edit-exception-checkbox"]') + .getDOMNode() + ).toBeDisabled(); + }); + }); +}); 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 e1352ac38dc49..a2c8531def2ba 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 @@ -137,7 +137,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({ setShouldDisableBulkClose( entryHasListType(exceptionItemsToAdd) || entryHasNonEcsType(exceptionItemsToAdd, signalIndexPatterns) || - exceptionItemsToAdd.length === 0 + exceptionItemsToAdd.every((item) => item.entries.length === 0) ); } }, [ @@ -259,7 +259,8 @@ export const EditExceptionModal = memo(function EditExceptionModal({ - + {i18n.ENDPOINT_QUARANTINE_TEXT} @@ -292,6 +293,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({ {i18n.CANCEL}