diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/utils/exception_list_items/index.ts b/x-pack/solutions/security/plugins/security_solution/public/common/utils/exception_list_items/index.ts index 1dc32749bc6d5..b032b6982eee9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/utils/exception_list_items/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/utils/exception_list_items/index.ts @@ -10,3 +10,5 @@ export { entriesToConditionEntriesMap, entriesToConditionEntries, } from './mappers'; + +export { parseListIdsFromImportedFile } from './list_id_parser'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/utils/exception_list_items/list_id_parser.ts b/x-pack/solutions/security/plugins/security_solution/public/common/utils/exception_list_items/list_id_parser.ts new file mode 100644 index 0000000000000..6c6b9ef4b8497 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/utils/exception_list_items/list_id_parser.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Helper function to extract list ids from an imported file. Does not check whether + * the file is valid, just tries to find list_id fields, so it can be used on UI side + * as a pre-check to ensure only the correct artifact type is being imported. + * + * @param file {File} file to extract list ids from + * @returns {Promise>} set of list ids found in the file + */ +export const parseListIdsFromImportedFile = async (file: File): Promise> => + (await file.text()) + .split('\n') + .filter((x) => x.trim() !== '') + .reduce((acc, line) => { + try { + const parsedItem = JSON.parse(line); + + if (parsedItem.list_id) { + acc.add(parsedItem.list_id); + } + } catch (e) { + // ignore parsing errors, the API will handle them and return an error for the line + } + + return acc; + }, new Set()); diff --git a/x-pack/solutions/security/plugins/security_solution/public/exceptions/components/import_exceptions_list_flyout/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/exceptions/components/import_exceptions_list_flyout/index.tsx index 3cb6c51e16394..93806adad554a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/exceptions/components/import_exceptions_list_flyout/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/exceptions/components/import_exceptions_list_flyout/index.tsx @@ -34,10 +34,15 @@ import type { BulkErrorSchema, ImportExceptionsResponseSchema, } from '@kbn/securitysolution-io-ts-list-types'; -import { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants'; +import { + ENDPOINT_ARTIFACT_LIST_IDS, + ENDPOINT_ARTIFACT_LISTS, +} from '@kbn/securitysolution-list-constants'; import type { HttpSetup } from '@kbn/core-http-browser'; import type { ToastInput, Toast, ErrorToastOptions } from '@kbn/core-notifications-browser'; +import { parseListIdsFromImportedFile } from '../../../common/utils/exception_list_items'; +import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { useImportExceptionList } from '../../hooks/use_import_exception_list'; import * as i18n from '../../translations'; @@ -65,6 +70,9 @@ export const ImportExceptionListFlyout = React.memo( const [asNewList, setAsNewList] = useState(false); const [alreadyExistingItem, setAlreadyExistingItem] = useState(false); const [endpointListImporting, setEndpointListImporting] = useState(false); + const isEndpointExceptionsMovedFFEnabled = useIsExperimentalFeatureEnabled( + 'endpointExceptionsMovedUnderManagement' + ); const resetForm = useCallback(() => { if (filePickerRef.current?.fileInput) { @@ -80,8 +88,21 @@ export const ImportExceptionListFlyout = React.memo( const { start: importExceptionList, ...importExceptionListState } = useImportExceptionList(); const ctrl = useRef(new AbortController()); - const handleImportExceptionList = useCallback(() => { + const handleImportExceptionList = useCallback(async () => { if (!importExceptionListState.loading && files) { + if (isEndpointExceptionsMovedFFEnabled) { + for (const file of Array.from(files)) { + const listIds = await parseListIdsFromImportedFile(file); + + if (ENDPOINT_ARTIFACT_LIST_IDS.some((id) => listIds.has(id))) { + addError(new Error(i18n.IMPORT_ENDPOINT_ARTIFACTS_ERROR_TEXT), { + title: i18n.UPLOAD_ERROR, + }); + return; + } + } + } + ctrl.current = new AbortController(); Array.from(files).forEach((file) => @@ -95,7 +116,16 @@ export const ImportExceptionListFlyout = React.memo( }) ); } - }, [asNewList, files, http, importExceptionList, importExceptionListState.loading, overwrite]); + }, [ + importExceptionListState.loading, + files, + isEndpointExceptionsMovedFFEnabled, + addError, + importExceptionList, + http, + overwrite, + asNewList, + ]); const handleImportSuccess = useCallback( (response: ImportExceptionsResponseSchema) => { @@ -137,6 +167,7 @@ export const ImportExceptionListFlyout = React.memo( if (err.error.message.includes('already exists')) { setAlreadyExistingItem(true); if ( + !isEndpointExceptionsMovedFFEnabled && err.error.message.includes( `Found that list_id: "${ENDPOINT_ARTIFACT_LISTS.endpointExceptions.id}" already exists` ) @@ -157,11 +188,23 @@ export const ImportExceptionListFlyout = React.memo( importExceptionListState.loading, importExceptionListState?.result, importExceptionListState?.result?.errors, + isEndpointExceptionsMovedFFEnabled, ]); + const handleFileChange = useCallback((inputFiles: FileList | null) => { setFiles(inputFiles ?? null); }, []); + const handleNewListCheckboxChange = useCallback(() => { + setAsNewList((prev) => !prev); + setOverwrite(false); + }, []); + + const handleOverwriteCheckboxChange = useCallback((): void => { + setOverwrite((prev) => !prev); + setAsNewList(false); + }, []); + const importExceptionListFlyoutTitleId = useGeneratedHtmlId({ prefix: 'importExceptionListFlyoutTitle', }); @@ -200,27 +243,31 @@ export const ImportExceptionListFlyout = React.memo( label={i18n.IMPORT_EXCEPTION_LIST_OVERWRITE} checked={overwrite} data-test-subj="importExceptionListOverwriteExistingCheckbox" - onChange={(e) => { - setOverwrite(!overwrite); - setAsNewList(false); - }} + onChange={handleOverwriteCheckboxChange} /> - + {isEndpointExceptionsMovedFFEnabled ? ( { - setAsNewList(!asNewList); - setOverwrite(false); - }} + onChange={handleNewListCheckboxChange} /> - + ) : ( + + + + )} )} diff --git a/x-pack/solutions/security/plugins/security_solution/public/exceptions/translations/shared_list.ts b/x-pack/solutions/security/plugins/security_solution/public/exceptions/translations/shared_list.ts index 5d7c8e7163964..550e90a640137 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/exceptions/translations/shared_list.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/exceptions/translations/shared_list.ts @@ -213,6 +213,14 @@ export const IMPORT_EXCEPTION_LIST_AS_NEW_LIST = i18n.translate( } ); +export const IMPORT_ENDPOINT_ARTIFACTS_ERROR_TEXT = i18n.translate( + 'xpack.securitySolution.exceptionsTable.importEndpointArtifactsErrorText', + { + defaultMessage: + 'You can only import shared exception lists here, but at least one of the imported files contains endpoint artifacts. Import endpoint artifacts from their dedicated pages instead.', + } +); + export const IMPORT_EXCEPTION_ENDPOINT_LIST_WARNING = i18n.translate( 'xpack.securitySolution.exceptionsTable.importExceptionEndpointListWarning', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.tsx index 2a915f5a7b5ff..ec3825c8ead4a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.tsx @@ -7,7 +7,10 @@ import React, { memo, useCallback, useMemo, useState, useEffect } from 'react'; -import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import type { + BulkErrorSchema, + ExceptionListItemSchema, +} from '@kbn/securitysolution-io-ts-list-types'; import { EuiButton, EuiFlexGroup, EuiSpacer, EuiText } from '@elastic/eui'; import type { EuiFlyoutSize } from '@elastic/eui/src/components/flyout/flyout'; import { useLocation } from 'react-router-dom'; @@ -52,6 +55,7 @@ import { BackToExternalAppButton } from '../back_to_external_app_button'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { ArtifactImportFlyout } from './components/artifact_import_flyout'; import { useIsImportFlyoutOpened } from './hooks/use_is_import_flyout_opened'; +import { ArtifactImportErrorsModal } from './components/artifact_import_errors_modal'; type ArtifactEntryCardType = typeof ArtifactEntryCard; @@ -118,6 +122,8 @@ export const ArtifactListPage = memo( useIsImportFlyoutOpened(allowCardCreateAction) && areEndpointExceptionsMovedUnderManagementFFEnabled; + const [importErrors, setImportErrors] = useState(undefined); + const setUrlParams = useSetUrlParams(); const { urlParams: { filter, includedPolicies }, @@ -298,6 +304,12 @@ export const ArtifactListPage = memo( refetchListData(); }, [closeImportFlyout, refetchListData]); + const handleImportFlyoutOnShowErrors = useCallback((errors: BulkErrorSchema[]) => { + setImportErrors(errors); + }, []); + + const handleCloseImportErrorsModal = useCallback(() => setImportErrors(undefined), []); + const description = useMemo(() => { const subtitleText = labels.pageAboutInfo ? ( {labels.pageAboutInfo} @@ -409,11 +421,16 @@ export const ArtifactListPage = memo( )} + {importErrors && ( + + )} + {selectedItemForDelete && ( void; + onConfirm: () => void; + isLoading: boolean; + 'data-test-subj'?: string; +} + +export const ArtifactImportConfirmModal: React.FC = ({ + onCancel, + onConfirm, + isLoading, + 'data-test-subj': dataTestSubj = 'artifactImportConfirmModal', +}) => { + const getTestId = useTestIdGenerator(dataTestSubj); + const modalTitleId = useGeneratedHtmlId(); + + return ( + + + + {i18n.translate('xpack.securitySolution.artifactListPage.importConfirmModal.title', { + defaultMessage: 'Import artifacts?', + })} + + + + +

+ {i18n.translate('xpack.securitySolution.artifactListPage.importConfirmModal.info', { + defaultMessage: + "This will add new artifacts to your list. If an artifact you're importing already exists, the existing version will be kept, and the import of that artifact will be skipped.", + })} +

+
+
+ + + {i18n.translate( + 'xpack.securitySolution.artifactListPage.importConfirmModal.cancelButtonTitle', + { defaultMessage: 'Cancel' } + )} + + + + {i18n.translate( + 'xpack.securitySolution.artifactListPage.importConfirmModal.confirmButtonTitle', + { defaultMessage: 'Import' } + )} + + +
+ ); +}; + +ArtifactImportConfirmModal.displayName = 'ArtifactImportConfirmModal'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_errors_modal.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_errors_modal.test.tsx new file mode 100644 index 0000000000000..2de2282c65aee --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_errors_modal.test.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { ArtifactListPageProps } from '../artifact_list_page'; +import userEvent from '@testing-library/user-event'; +import { + createAppRootMockRenderer, + type AppContextTestRender, +} from '../../../../common/mock/endpoint'; +import type { ArtifactImportErrorsModalProps } from './artifact_import_errors_modal'; +import { ArtifactImportErrorsModal } from './artifact_import_errors_modal'; + +describe('When the flyout is opened in the ArtifactListPage component', () => { + let render: ( + props?: Partial + ) => Promise>; + let renderResult: ReturnType; + let props: ArtifactImportErrorsModalProps; + + beforeEach(() => { + const mockedContext = createAppRootMockRenderer(); + + props = { + errors: [], + onClose: jest.fn(), + }; + + render = async () => { + renderResult = mockedContext.render( + + ); + return renderResult; + }; + }); + + it('should display `Close` button', async () => { + await render(); + + expect(renderResult.getByTestId('testModal')).toBeInTheDocument(); + }); + + it('should call `onClose` when the `Close` button is clicked', async () => { + await render(); + + await userEvent.click(renderResult.getByTestId('testModal-closeButton')); + + expect(props.onClose).toHaveBeenCalled(); + }); + + it('should display the list of errors with item ids and messages', async () => { + props.errors = [ + { + item_id: 'item1', + error: { message: 'Error message 1', status_code: 403 }, + }, + { + item_id: 'item2', + error: { message: 'Error message 2', status_code: 403 }, + }, + ]; + + await render(); + + expect(renderResult.getByText('Import errors')).toBeInTheDocument(); + expect(renderResult.getByText('item (item1):')).toBeInTheDocument(); + expect(renderResult.getByText('Error message 1')).toBeInTheDocument(); + expect(renderResult.getByText('item (item2):')).toBeInTheDocument(); + expect(renderResult.getByText('Error message 2')).toBeInTheDocument(); + }); + + it('should handle the case when item_id is undefined', async () => { + props.errors = [ + { + error: { message: 'Error message without item_id', status_code: 403 }, + }, + ]; + + await render(); + + expect(renderResult.getByText('Import errors')).toBeInTheDocument(); + expect(renderResult.getByText('item (undefined):')).toBeInTheDocument(); + expect(renderResult.getByText('Error message without item_id')).toBeInTheDocument(); + expect(renderResult.getByText(/Error message /)).toBeInTheDocument(); + }); + + it('should remove "EndpointArtifactError: " prefix from error messages', async () => { + props.errors = [ + { + item_id: 'item1', + error: { + message: 'EndpointArtifactError: This is an endpoint artifact error message', + status_code: 403, + }, + }, + ]; + + await render(); + + expect(renderResult.getByText('Import errors')).toBeInTheDocument(); + expect(renderResult.getByText('item (item1):')).toBeInTheDocument(); + expect( + renderResult.getByText(/^This is an endpoint artifact error message/) + ).toBeInTheDocument(); + expect(renderResult.queryByText(/EndpointArtifactError/)).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_errors_modal.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_errors_modal.tsx new file mode 100644 index 0000000000000..d95bd5a746742 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_errors_modal.tsx @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSpacer, + EuiText, + useGeneratedHtmlId, +} from '@elastic/eui'; +import type { BulkErrorSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { useTestIdGenerator } from '../../../hooks/use_test_id_generator'; + +export interface ArtifactImportErrorsModalProps { + errors: BulkErrorSchema[]; + onClose: () => void; + 'data-test-subj'?: string; +} + +export const ArtifactImportErrorsModal: React.FC = ({ + errors, + onClose, + 'data-test-subj': dataTestSubj = 'artifactImportErrorsModal', +}) => { + const getTestId = useTestIdGenerator(dataTestSubj); + const modalTitleId = useGeneratedHtmlId(); + + const errorsToDisplay: Array<{ itemId: string; message: string }> = useMemo( + () => + errors.map((error) => ({ + itemId: error.item_id ?? 'undefined', + message: error.error.message.replace('EndpointArtifactError: ', ''), + })), + [errors] + ); + + return ( + + + + {i18n.translate('xpack.securitySolution.artifactListPage.importErrorsModal.title', { + defaultMessage: 'Import errors', + })} + + + + +

+ {i18n.translate('xpack.securitySolution.artifactListPage.importErrorsModal.info', { + defaultMessage: + "Some items couldn't be imported. Review the errors below for details.", + })} +

+
+ + + + ({ + maxHeight: '50vh', + overflowY: 'auto', + border: `1px solid ${euiTheme.euiTheme.colors.borderBasePlain}`, + })} + direction="column" + > + + +
    + {errorsToDisplay.map((error, index) => ( +
  1. ({ + '&::marker': { fontWeight: 'bold' }, + margin: euiTheme.euiTheme.size.m, + })} + key={index} + > + + {i18n.translate( + 'xpack.securitySolution.artifactListPage.importErrorsModal.item', + { + defaultMessage: 'item ({itemId}):', + values: { itemId: error.itemId }, + } + )} + {' '} + {error.message} +
  2. + ))} +
+
+
+
+
+ + + {i18n.translate( + 'xpack.securitySolution.artifactListPage.importErrorsModal.closeButtonTitle', + { defaultMessage: 'Close' } + )} + + +
+ ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.test.tsx index 83d265050c53e..3d41673b071e4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.test.tsx @@ -31,6 +31,7 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { let mockedTrustedAppApi: ReturnType; let props: ArtifactImportFlyoutProps; let ui: ReturnType; + let currentListId: string; beforeEach(() => { const mockedContext = createAppRootMockRenderer(); @@ -38,11 +39,15 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { mockedTrustedAppApi = trustedAppsAllHttpMocks(coreStart.http); + const apiClient = new TrustedAppsApiClient(coreStart.http); + currentListId = apiClient.listId; + props = { labels: artifactListPageLabels, - apiClient: new TrustedAppsApiClient(coreStart.http), + apiClient, onCancel: jest.fn(), onSuccess: jest.fn(), + onShowErrors: jest.fn(), }; render = async () => { @@ -80,26 +85,47 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { it('should enable `Import` button when a file is selected', async () => { await render(); - await ui.uploadFile(); + await ui.uploadFile([currentListId]); expect(ui.getImportButton()).toBeEnabled(); }); - it('should call the import API when `Import` button is clicked', async () => { + it('should show a confirmation modal when `Import` button is clicked', async () => { await render(); - await ui.uploadFile(); + await ui.uploadFile([currentListId]); await userEvent.click(ui.getImportButton()); + expect(ui.queryConfirmModal()).toBeInTheDocument(); + }); + + it("should close the confirmation modal but keep the flyout open when the modal's cancel button is clicked", async () => { + await render(); + + await ui.uploadFile([currentListId]); + await userEvent.click(ui.getImportButton()); + await userEvent.click(ui.getConfirmModalCancelButton()); + + expect(ui.queryConfirmModal()).not.toBeInTheDocument(); + expect(ui.queryImportFlyout()).toBeInTheDocument(); + }); + + it('should call the import API when the modal is confirmed', async () => { + await render(); + + await ui.uploadFile([currentListId]); + await userEvent.click(ui.getImportButton()); + await userEvent.click(ui.getConfirmModalConfirmButton()); + expect(mockedTrustedAppApi.responseProvider.trustedAppImportList).toHaveBeenCalledWith( expect.objectContaining({ version: '2023-10-31', - query: { overwrite: true } as HttpFetchOptionsWithPath['query'], + query: { overwrite: false, as_new_list: false } as HttpFetchOptionsWithPath['query'], }) ); }); - it('should disable `Import` button while the import is in progress', async () => { + it('should disable `Import` button on the modal and the flyout while the import is in progress', async () => { const deferrable = getDeferred(); mockedTrustedAppApi.responseProvider.trustedAppImportList.mockDelay.mockReturnValue( deferrable.promise @@ -107,39 +133,183 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { await render(); - await ui.uploadFile(); + await ui.uploadFile([currentListId]); await userEvent.click(ui.getImportButton()); + await userEvent.click(ui.getConfirmModalConfirmButton()); - expect(ui.getImportButton()).toBeDisabled(); + await waitFor(() => { + expect(ui.getImportButton()).toBeDisabled(); + expect(ui.getConfirmModalConfirmButton()).toBeDisabled(); + }); }); - it('should show a success toast and call `onSuccess` after a successful import', async () => { - await render(); + describe('when handling server response', () => { + const LIST_CONFLICT_ERROR = { + error: { + status_code: 409, + message: + 'Found that list_id: "endpoint_list" already exists. Import of list_id: "endpoint_list" skipped.', + }, + list_id: 'endpoint_list', + }; - await ui.uploadFile(); - await userEvent.click(ui.getImportButton()); + const ITEM_CONFLICT_ERROR = { + error: { + status_code: 409, + message: + 'Found that item_id: "0d82595f-f79d-48c8-8522-7715e1640884" already exists. Import of item_id: "0d82595f-f79d-48c8-8522-7715e1640884" skipped.', + }, + list_id: 'endpoint_list', + item_id: '0d82595f-f79d-48c8-8522-7715e1640884', + }; + + const ITEM_ENDPOINT_ARTIFACT_ERROR = { + error: { + status_code: 403, + message: + "EndpointArtifactError: This artifact can't be imported because it belongs to a space you don't have access to. Update the artifact in its original space and try again.", + }, + list_id: 'endpoint_list', + item_id: '1a0200db-3dd7-46f4-bb4d-f23904559c32', + }; - expect(coreStart.notifications.toasts.addSuccess).toHaveBeenCalledWith({ - text: '2 artifacts imported.', - title: 'Artifact list imported successfully', - toastLifeTimeMs: 60_000, + it('should show a success toast and call `onSuccess` after a successful import', async () => { + await render(); + + await ui.uploadFile([currentListId]); + await userEvent.click(ui.getImportButton()); + await userEvent.click(ui.getConfirmModalConfirmButton()); + + expect(coreStart.notifications.toasts.addSuccess).toHaveBeenCalledWith({ + title: 'Artifacts imported', + text: 'All artifacts were imported successfully.', + toastLifeTimeMs: 60_000, + }); + expect(props.onSuccess).toHaveBeenCalled(); }); - expect(props.onSuccess).toHaveBeenCalled(); - }); - it('should show an error toast if the import API fails', async () => { - mockedTrustedAppApi.responseProvider.trustedAppImportList.mockImplementation(() => { - throw new Error('Fail message from server'); + it('should not care about list conflict in response', async () => { + mockedTrustedAppApi.responseProvider.trustedAppImportList.mockImplementation(() => ({ + errors: [LIST_CONFLICT_ERROR], + success: false, + success_count: 2, + success_exception_lists: false, + success_count_exception_lists: 0, + success_exception_list_items: true, + success_count_exception_list_items: 2, + })); + + await render(); + + await ui.uploadFile([currentListId]); + await userEvent.click(ui.getImportButton()); + await userEvent.click(ui.getConfirmModalConfirmButton()); + + expect(coreStart.notifications.toasts.addSuccess).toHaveBeenCalledWith({ + title: 'Artifacts imported', + text: 'All artifacts were imported successfully.', + toastLifeTimeMs: 60_000, + }); + expect(props.onSuccess).toHaveBeenCalled(); }); - await render(); + it('should show a warning toast when some of the items were not imported', async () => { + mockedTrustedAppApi.responseProvider.trustedAppImportList.mockImplementation(() => ({ + errors: [LIST_CONFLICT_ERROR, ITEM_CONFLICT_ERROR, ITEM_ENDPOINT_ARTIFACT_ERROR], + success: false, + success_count: 3, + success_exception_lists: false, + success_count_exception_lists: 0, + success_exception_list_items: false, + success_count_exception_list_items: 3, // there are some successful imports + })); + + await render(); + + await ui.uploadFile([currentListId]); + await userEvent.click(ui.getImportButton()); + await userEvent.click(ui.getConfirmModalConfirmButton()); + + expect(coreStart.notifications.toasts.addWarning).toHaveBeenCalledWith({ + title: 'Import completed with errors', + toastLifeTimeMs: 60_000, + text: expect.any(Function), + }); + expect(props.onSuccess).toHaveBeenCalled(); + }); - await ui.uploadFile(); - await userEvent.click(ui.getImportButton()); + it('should show a danger toast when none of the items were imported', async () => { + mockedTrustedAppApi.responseProvider.trustedAppImportList.mockImplementation(() => ({ + errors: [LIST_CONFLICT_ERROR, ITEM_CONFLICT_ERROR, ITEM_ENDPOINT_ARTIFACT_ERROR], + success: false, + success_count: 3, + success_exception_lists: false, + success_count_exception_lists: 0, + success_exception_list_items: false, + success_count_exception_list_items: 0, // there are no successful imports + })); + + await render(); + + await ui.uploadFile([currentListId]); + await userEvent.click(ui.getImportButton()); + await userEvent.click(ui.getConfirmModalConfirmButton()); + + expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith({ + title: "Artifacts weren't imported", + toastLifeTimeMs: 60_000, + text: expect.any(Function), + }); + expect(props.onSuccess).toHaveBeenCalled(); + }); - expect(coreStart.notifications.toasts.addError).toHaveBeenCalledWith( - expect.objectContaining(new Error('Fail message from server')), - { title: 'Artifact list import failed', toastMessage: 'Fail message from server' } - ); + it('should show an danger toast and close the modal if another list is being imported', async () => { + await render(); + + await ui.uploadFile(['some-other-list-id']); + await userEvent.click(ui.getImportButton()); + await userEvent.click(ui.getConfirmModalConfirmButton()); + + expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith({ + title: artifactListPageLabels.pageImportErrorToastTitle, + text: artifactListPageLabels.pageImportOnlyCurrentArtifactCanBeImportedError, + }); + expect(ui.queryConfirmModal()).not.toBeInTheDocument(); + }); + + it('should show an error toast if not only the current artifact type is included in the import file', async () => { + await render(); + + await ui.uploadFile(['some-other-list-id', currentListId]); + await userEvent.click(ui.getImportButton()); + await userEvent.click(ui.getConfirmModalConfirmButton()); + + expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith({ + title: artifactListPageLabels.pageImportErrorToastTitle, + text: artifactListPageLabels.pageImportOnlyCurrentArtifactCanBeImportedError, + }); + expect(ui.queryConfirmModal()).not.toBeInTheDocument(); + }); + + it('should show an error toast if the import API fails', async () => { + mockedTrustedAppApi.responseProvider.trustedAppImportList.mockImplementation(() => { + throw new Error('Fail message from server'); + }); + + await render(); + + await ui.uploadFile([currentListId]); + await userEvent.click(ui.getImportButton()); + await userEvent.click(ui.getConfirmModalConfirmButton()); + + expect(coreStart.notifications.toasts.addError).toHaveBeenCalledWith( + expect.objectContaining(new Error('Fail message from server')), + { + title: artifactListPageLabels.pageImportErrorToastTitle, + toastMessage: 'Fail message from server', + } + ); + expect(ui.queryConfirmModal()).not.toBeInTheDocument(); + }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.tsx index 4d0cb0edf1624..b9d897326d927 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_import_flyout.tsx @@ -21,36 +21,87 @@ import { useGeneratedHtmlId, } from '@elastic/eui'; import React, { useCallback } from 'react'; -import { useToasts } from '../../../../common/lib/kibana'; -import type { ArtifactListPageLabels } from '../translations'; +import { toMountPoint } from '@kbn/react-kibana-mount'; +import type { BulkErrorSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { parseListIdsFromImportedFile } from '../../../../common/utils/exception_list_items'; +import { useKibana, useToasts } from '../../../../common/lib/kibana'; +import type { artifactListPageLabels } from '../translations'; import { useImportArtifactList } from '../../../hooks/artifacts/use_import_artifact_list'; import type { ExceptionsListApiClient } from '../../../services/exceptions_list/exceptions_list_api_client'; import { useTestIdGenerator } from '../../../hooks/use_test_id_generator'; +import { ArtifactImportConfirmModal } from './artifact_import_confirm_modal'; export interface ArtifactImportFlyoutProps { onCancel: () => void; onSuccess: () => void; + onShowErrors: (errors: BulkErrorSchema[]) => void; apiClient: ExceptionsListApiClient; - labels: ArtifactListPageLabels; + labels: typeof artifactListPageLabels; 'data-test-subj'?: string; } +const ArtifactImportErrorToast: React.FC<{ + text: string; + buttonLabel: string; + errors: BulkErrorSchema[]; + onShowErrors: (errors: BulkErrorSchema[]) => void; +}> = ({ text, buttonLabel, errors, onShowErrors }) => { + const handleOnClick = useCallback(() => { + onShowErrors(errors); + }, [errors, onShowErrors]); + + return ( + <> + {text} + + + + {buttonLabel} + + + + ); +}; + export const ArtifactImportFlyout: React.FC = ({ onCancel, onSuccess, + onShowErrors, apiClient, labels, 'data-test-subj': dataTestSubj = 'artifactImportFlyout', }) => { const toasts = useToasts(); + const services = useKibana().services; const getTestId = useTestIdGenerator(dataTestSubj); const [file, setFile] = React.useState(null); + const [showConfirmModal, setShowConfirmModal] = React.useState(false); const { isLoading, mutate } = useImportArtifactList(apiClient); const handleOnSubmit = useCallback(() => { + setShowConfirmModal(true); + }, []); + + const handleOnCancelModal = useCallback(() => { + setShowConfirmModal(false); + }, []); + + const handleOnConfirmModal = useCallback(async () => { if (file !== null) { + const listIds = await parseListIdsFromImportedFile(file); + + if (listIds.size > 1 || !listIds.has(apiClient.listId)) { + toasts.addDanger({ + title: labels.pageImportErrorToastTitle, + text: labels.pageImportOnlyCurrentArtifactCanBeImportedError, + }); + setShowConfirmModal(false); + + return; + } + mutate( { file }, { @@ -59,30 +110,67 @@ export const ArtifactImportFlyout: React.FC = ({ title: labels.pageImportErrorToastTitle, toastMessage: error.body?.message || error.message, }); + setShowConfirmModal(false); }, onSuccess: (response) => { - toasts.addSuccess({ - title: labels.pageImportSuccessToastTitle, - text: `${labels.getPageImportSuccessToastText?.( - response.success_count_exception_list_items - )}${ - response.errors.length - ? ` ${response.errors.length} errors happened: ${response.errors - .map( - (item, index) => - `(${index + 1}) item (${item.item_id}): ${item.error.message}.` - ) - .join(' -- ')}` - : '' - }`, - toastLifeTimeMs: 60_000, - }); + if (response.success_exception_list_items === true) { + toasts.addSuccess({ + title: labels.pageImportSuccessToastTitle, + text: labels.pageImportSuccessToastText, + toastLifeTimeMs: 60_000, + }); + } else { + const itemErrors: BulkErrorSchema[] = response.errors.filter( + (error) => + !( + error.error.status_code === 409 && + error.error.message.match(/Found that list_id: "\w+" already exists/) + ) + ); + + if (itemErrors.length > 0 && response.success_count_exception_list_items > 0) { + toasts.addWarning({ + title: labels.pageImportCompletedWithErrorsToastTitle, + toastLifeTimeMs: 60_000, + text: toMountPoint( + , + services + ), + }); + } + + if (itemErrors.length > 0 && response.success_count_exception_list_items === 0) { + toasts.addDanger({ + title: labels.pageImportErrorToastTitle, + toastLifeTimeMs: 60_000, + text: toMountPoint( + , + services + ), + }); + } + } + + setShowConfirmModal(false); onSuccess(); }, } ); } - }, [file, labels, mutate, onSuccess, toasts]); + }, [apiClient.listId, file, labels, mutate, onShowErrors, onSuccess, services, toasts]); const handleOnFileChange: EuiFilePickerProps['onChange'] = useCallback( (files: FileList | null) => { @@ -107,6 +195,15 @@ export const ArtifactImportFlyout: React.FC = ({ aria-labelledby={importEndpointArtifactListFlyoutTitleId} data-test-subj={getTestId()} > + {showConfirmModal && ( + + )} +

{labels.pageImportButtonTitle}

diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/integration_tests/artifact_list_page.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/integration_tests/artifact_list_page.test.tsx index 740cf9e422fd4..0a3575481c6c9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/integration_tests/artifact_list_page.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/integration_tests/artifact_list_page.test.tsx @@ -20,6 +20,7 @@ import { import { getDeferred } from '../../../mocks/utils'; import { useGetEndpointSpecificPolicies } from '../../../services/policies/hooks'; import type { ArtifactEntryCardDecoratorProps } from '../../artifact_entry_card'; +import { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants'; jest.mock('../../../services/policies/hooks', () => ({ useGetEndpointSpecificPolicies: jest.fn(), @@ -231,10 +232,11 @@ describe('When using the ArtifactListPage component', () => { await userEvent.click(importExportUi.getMenuButton()); await userEvent.click(importExportUi.getImportButton()); - await importFlyoutUi.uploadFile(); + await importFlyoutUi.uploadFile([ENDPOINT_ARTIFACT_LISTS.trustedApps.id]); const currentApiCallCount = mockedApi.responseProvider.trustedAppsList.mock.calls.length; await userEvent.click(importFlyoutUi.getImportButton()); + await userEvent.click(importFlyoutUi.getConfirmModalConfirmButton()); await waitFor(() => { expect(mockedApi.responseProvider.trustedAppsList).toHaveBeenCalledTimes( diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/mocks.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/mocks.tsx index a9aefa1e40168..3b7144eae3065 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/mocks.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/mocks.tsx @@ -168,12 +168,29 @@ export const getArtifactImportFlyoutUiMocks = ( const queryImportFlyout = () => renderResult.queryByTestId(dataTestSubj); const getCancelButton = () => renderResult.getByTestId(`${dataTestSubj}-cancelButton`); const getImportButton = () => renderResult.getByTestId(`${dataTestSubj}-importButton`); + const queryConfirmModal = () => renderResult.queryByTestId(`${dataTestSubj}-confirmModal`); + const getConfirmModalConfirmButton = () => + renderResult.getByTestId(`${dataTestSubj}-confirmModal-confirmButton`); + const getConfirmModalCancelButton = () => + renderResult.getByTestId(`${dataTestSubj}-confirmModal-cancelButton`); - const uploadFile = () => + const uploadFile = (listIds: string[]) => userEvent.upload( renderResult.getByTestId(`${dataTestSubj}-filePicker`), - new File(['random file content'], 'trusted_apps.ndjson') + new File( + // every id is duplicated to simulate multiple lines. plus one invalid line to make sure parsing errors are ignored + [listIds.map((id) => `{"list_id":"${id}"}\n{"list_id":"${id}"}\ninvalid line`).join('\n')], + 'trusted_apps.ndjson' + ) ); - return { queryImportFlyout, getCancelButton, getImportButton, uploadFile }; + return { + queryImportFlyout, + getCancelButton, + getImportButton, + queryConfirmModal, + getConfirmModalConfirmButton, + getConfirmModalCancelButton, + uploadFile, + }; }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/translations.ts index 2307fd6f13c42..0c7ba43bd9c45 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/artifact_list_page/translations.ts @@ -26,7 +26,7 @@ export const artifactListPageLabels = Object.freeze({ pageImportButtonTitle: i18n.translate( 'xpack.securitySolution.artifactListPage.importButtonTitle', { - defaultMessage: 'Import artifact list', + defaultMessage: 'Import artifacts', } ), pageExportButtonTitle: i18n.translate( @@ -50,32 +50,68 @@ export const artifactListPageLabels = Object.freeze({ pageImportSuccessToastTitle: i18n.translate( 'xpack.securitySolution.artifactListPage.importSuccessToastTitle', { - defaultMessage: 'Artifact list imported successfully', + defaultMessage: 'Artifacts imported', + } + ), + pageImportSuccessToastText: i18n.translate( + 'xpack.securitySolution.artifactListPage.importSuccessToastText', + { + defaultMessage: 'All artifacts were imported successfully.', + } + ), + pageImportCompletedWithErrorsToastTitle: i18n.translate( + 'xpack.securitySolution.artifactListPage.importCompletedWithErrorsToastTitle', + { + defaultMessage: 'Import completed with errors', + } + ), + getPageImportCompletedWithErrorsToastText: ( + importedCount: number, + failedCount: number + ): string => { + return i18n.translate( + 'xpack.securitySolution.artifactListPage.importCompletedWithErrorsToastText', + { + defaultMessage: + '{importedCount} imported, {failedCount} failed. Review the errors for details.', + values: { importedCount, failedCount }, + } + ); + }, + pageImportAllItemsFailedToastText: i18n.translate( + 'xpack.securitySolution.artifactListPage.importAllItemsFailedToastText', + { + defaultMessage: "The artifacts couldn't be imported. Review the errors and try again.", + } + ), + pageImportViewErrorsButton: i18n.translate( + 'xpack.securitySolution.artifactListPage.importViewErrorsButton', + { + defaultMessage: 'View errors', } ), - getPageImportSuccessToastText: (successCount: number): string => - i18n.translate('xpack.securitySolution.artifactListPage.importSuccessToastText', { - defaultMessage: - '{successCount} {successCount, plural, one {artifact} other {artifacts}} imported.', - values: { successCount }, - }), pageImportErrorToastTitle: i18n.translate( 'xpack.securitySolution.artifactListPage.importErrorToastTitle', { - defaultMessage: 'Artifact list import failed', + defaultMessage: "Artifacts weren't imported", + } + ), + pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( + 'xpack.securitySolution.artifactListPage.importOnlyCurrentArtifactCanBeImportedToastMessage', + { + defaultMessage: 'You can only import the current artifact type here.', } ), importFlyoutDetails: i18n.translate( 'xpack.securitySolution.artifactListPage.importFlyoutDetails', { - defaultMessage: - 'Attention: importing your artifacts will overwrite the existing list, which results in losing all existing artifacts that can be edited by the current user.', + defaultMessage: 'Import artifacts to your artifact list.', } ), importFlyoutImportSubmitButtonLabel: i18n.translate( 'xpack.securitySolution.artifactListPage.importFlyoutImportSubmitButtonLabel', { - defaultMessage: 'Import list', + defaultMessage: 'Import artifacts', } ), @@ -100,7 +136,7 @@ export const artifactListPageLabels = Object.freeze({ ), emptyStateImportButtonLabel: i18n.translate( 'xpack.securitySolution.artifactListPage.emptyStateImportButtonLabel', - { defaultMessage: 'Import list' } + { defaultMessage: 'Import artifacts' } ), // ------------------------------ @@ -141,7 +177,7 @@ export const artifactListPageLabels = Object.freeze({ cardActionDeleteLabel: i18n.translate( 'xpack.securitySolution.artifactListPage.cardActionDeleteLabel', { - defaultMessage: 'Delete event filter', + defaultMessage: 'Delete artifact', } ), @@ -171,8 +207,7 @@ export type ArtifactListPageRequiredLabels = Pick< | 'pageExportButtonTitle' | 'pageExportSuccessToastTitle' | 'pageExportErrorToastTitle' - | 'pageImportSuccessToastTitle' - | 'pageImportErrorToastTitle' + | 'pageImportOnlyCurrentArtifactCanBeImportedError' | 'getShowingCountLabel' | 'cardActionEditLabel' | 'cardActionDeleteLabel' diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/hooks/artifacts/use_import_artifact_list.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/hooks/artifacts/use_import_artifact_list.test.tsx index 8447b5e667723..378b8f2537ccf 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/hooks/artifacts/use_import_artifact_list.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/hooks/artifacts/use_import_artifact_list.test.tsx @@ -69,13 +69,14 @@ describe('Import artifact list hook', () => { headers: { 'Content-Type': undefined }, body: expect.any(FormData), query: { - overwrite: true, + overwrite: false, + as_new_list: false, }, }) ); }); - it('should throw when importing an artifact list', async () => { + it('should throw when server responds with error', async () => { const expectedError = { response: { status: 500, diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx index 54269a6a191f7..39667dbcb58e9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx @@ -30,33 +30,27 @@ const BLOCKLIST_PAGE_LABELS: ArtifactListPageLabels = { defaultMessage: 'Add blocklist entry', }), pageImportButtonTitle: i18n.translate('xpack.securitySolution.blocklist.pageImportButtonTitle', { - defaultMessage: 'Import blocklist', + defaultMessage: 'Import blocklist entries', }), pageExportButtonTitle: i18n.translate('xpack.securitySolution.blocklist.pageExportButtonTitle', { - defaultMessage: 'Export blocklist', + defaultMessage: 'Export blocklist entries', }), pageExportSuccessToastTitle: i18n.translate( 'xpack.securitySolution.blocklist.pageExportSuccessToastTitle', { - defaultMessage: 'Blocklist exported successfully', + defaultMessage: 'Blocklist entries exported successfully', } ), pageExportErrorToastTitle: i18n.translate( 'xpack.securitySolution.blocklist.pageExportErrorToastTitle', { - defaultMessage: 'Blocklist export failed', + defaultMessage: 'Blocklist entries export failed', } ), - pageImportSuccessToastTitle: i18n.translate( - 'xpack.securitySolution.blocklist.pageImportSuccessToastTitle', + pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( + 'xpack.securitySolution.blocklist.pageImportOnlyCurrentArtifactCanBeImportedError', { - defaultMessage: 'Blocklist imported successfully', - } - ), - pageImportErrorToastTitle: i18n.translate( - 'xpack.securitySolution.blocklist.pageImportErrorToastTitle', - { - defaultMessage: 'Blocklist import failed', + defaultMessage: 'You can only import blocklist entries here.', } ), getShowingCountLabel: (total) => @@ -133,7 +127,7 @@ const BLOCKLIST_PAGE_LABELS: ArtifactListPageLabels = { ), emptyStateImportButtonLabel: i18n.translate( 'xpack.securitySolution.blocklist.emptyStateImportButtonLabel', - { defaultMessage: 'Import blocklist' } + { defaultMessage: 'Import blocklist entries' } ), searchPlaceholderInfo: i18n.translate('xpack.securitySolution.blocklist.searchPlaceholderInfo', { defaultMessage: 'Search on the fields below: name, description, value', diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/endpoint_exceptions/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/endpoint_exceptions/translations.tsx index d58bbb82837b6..fd43d985d5a0b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/endpoint_exceptions/translations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/endpoint_exceptions/translations.tsx @@ -28,37 +28,31 @@ export const ENDPOINT_EXCEPTIONS_PAGE_LABELS: ArtifactListPageLabels = { pageImportButtonTitle: i18n.translate( 'xpack.securitySolution.endpointExceptions.pageImportButtonTitle', { - defaultMessage: 'Import endpoint exception list', + defaultMessage: 'Import Endpoint exceptions', } ), pageExportButtonTitle: i18n.translate( 'xpack.securitySolution.endpointExceptions.pageExportButtonTitle', { - defaultMessage: 'Export endpoint exception list', + defaultMessage: 'Export Endpoint exceptions', } ), pageExportSuccessToastTitle: i18n.translate( 'xpack.securitySolution.endpointExceptions.exportSuccessToastTitle', { - defaultMessage: 'Endpoint exception list exported successfully', + defaultMessage: 'Endpoint exceptions exported successfully', } ), pageExportErrorToastTitle: i18n.translate( 'xpack.securitySolution.endpointExceptions.exportErrorToastTitle', { - defaultMessage: 'Endpoint exception list export failed', + defaultMessage: 'Endpoint exceptions export failed', } ), - pageImportSuccessToastTitle: i18n.translate( - 'xpack.securitySolution.endpointExceptions.pageImportSuccessToastTitle', + pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( + 'xpack.securitySolution.endpointExceptions.pageImportOnlyCurrentArtifactCanBeImportedError', { - defaultMessage: 'Endpoint exception list imported successfully', - } - ), - pageImportErrorToastTitle: i18n.translate( - 'xpack.securitySolution.endpointExceptions.pageImportErrorToastTitle', - { - defaultMessage: 'Endpoint exception list import failed', + defaultMessage: 'You can only import Endpoint exceptions here.', } ), getShowingCountLabel: (total) => @@ -141,7 +135,7 @@ export const ENDPOINT_EXCEPTIONS_PAGE_LABELS: ArtifactListPageLabels = { ), emptyStateImportButtonLabel: i18n.translate( 'xpack.securitySolution.endpointExceptions.emptyStateImportButtonLabel', - { defaultMessage: 'Import endpoint exception list' } + { defaultMessage: 'Import Endpoint exceptions' } ), searchPlaceholderInfo: i18n.translate( 'xpack.securitySolution.endpointExceptions.searchPlaceholderInfo', diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.tsx index b52010aa45596..18c6e0639e601 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.tsx @@ -64,37 +64,31 @@ const EVENT_FILTERS_PAGE_LABELS: ArtifactListPageLabels = { pageImportButtonTitle: i18n.translate( 'xpack.securitySolution.eventFilters.pageImportButtonTitle', { - defaultMessage: 'Import event filter list', + defaultMessage: 'Import event filters', } ), pageExportButtonTitle: i18n.translate( 'xpack.securitySolution.eventFilters.pageExportButtonTitle', { - defaultMessage: 'Export event filter list', + defaultMessage: 'Export event filters', } ), pageExportSuccessToastTitle: i18n.translate( 'xpack.securitySolution.eventFilters.exportSuccessToastTitle', { - defaultMessage: 'Event filter list exported successfully', + defaultMessage: 'Event filters exported successfully', } ), pageExportErrorToastTitle: i18n.translate( 'xpack.securitySolution.eventFilters.exportErrorToastTitle', { - defaultMessage: 'Event filter list export failed', + defaultMessage: 'Event filters export failed', } ), - pageImportSuccessToastTitle: i18n.translate( - 'xpack.securitySolution.eventFilters.pageImportSuccessToastTitle', + pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( + 'xpack.securitySolution.eventFilters.pageImportOnlyCurrentArtifactCanBeImportedError', { - defaultMessage: 'Event filter list imported successfully', - } - ), - pageImportErrorToastTitle: i18n.translate( - 'xpack.securitySolution.eventFilters.pageImportErrorToastTitle', - { - defaultMessage: 'Event filter list import failed', + defaultMessage: 'You can only import event filters here.', } ), getShowingCountLabel: (total) => @@ -173,7 +167,7 @@ const EVENT_FILTERS_PAGE_LABELS: ArtifactListPageLabels = { ), emptyStateImportButtonLabel: i18n.translate( 'xpack.securitySolution.eventFilters.emptyStateImportButtonLabel', - { defaultMessage: 'Import event filter list' } + { defaultMessage: 'Import event filters' } ), searchPlaceholderInfo: i18n.translate( 'xpack.securitySolution.eventFilters.searchPlaceholderInfo', diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx index 215c3cca1c8ca..2575c733735f9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx @@ -32,37 +32,31 @@ const HOST_ISOLATION_EXCEPTIONS_LABELS: ArtifactListPageLabels = Object.freeze({ pageImportButtonTitle: i18n.translate( 'xpack.securitySolution.hostIsolationExceptions.pageImportButtonTitle', { - defaultMessage: 'Import host isolation exception list', + defaultMessage: 'Import host isolation exceptions', } ), pageExportButtonTitle: i18n.translate( 'xpack.securitySolution.hostIsolationExceptions.pageExportButtonTitle', { - defaultMessage: 'Export host isolation exception list', + defaultMessage: 'Export host isolation exceptions', } ), pageExportSuccessToastTitle: i18n.translate( 'xpack.securitySolution.hostIsolationExceptions.exportSuccessToastTitle', { - defaultMessage: 'Host isolation exception list exported successfully', + defaultMessage: 'Host isolation exceptions exported successfully', } ), pageExportErrorToastTitle: i18n.translate( 'xpack.securitySolution.hostIsolationExceptions.exportErrorToastTitle', { - defaultMessage: 'Host isolation exception list export failed', + defaultMessage: 'Host isolation exceptions export failed', } ), - pageImportSuccessToastTitle: i18n.translate( - 'xpack.securitySolution.hostIsolationExceptions.pageImportSuccessToastTitle', + pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( + 'xpack.securitySolution.hostIsolationExceptions.pageImportOnlyCurrentArtifactCanBeImportedError', { - defaultMessage: 'Host isolation exception list imported successfully', - } - ), - pageImportErrorToastTitle: i18n.translate( - 'xpack.securitySolution.hostIsolationExceptions.pageImportErrorToastTitle', - { - defaultMessage: 'Host isolation exception list import failed', + defaultMessage: 'You can only import host isolation exceptions here.', } ), getShowingCountLabel: (total) => @@ -145,7 +139,7 @@ const HOST_ISOLATION_EXCEPTIONS_LABELS: ArtifactListPageLabels = Object.freeze({ ), emptyStateImportButtonLabel: i18n.translate( 'xpack.securitySolution.hostIsolationExceptions.emptyStateImportButtonLabel', - { defaultMessage: 'Import host isolation exception list' } + { defaultMessage: 'Import host isolation exceptions' } ), searchPlaceholderInfo: i18n.translate( 'xpack.securitySolution.hostIsolationExceptions.searchPlaceholderInfo', diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/translations.ts index 670984541f837..426146a0c5025 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/translations.ts @@ -55,6 +55,6 @@ export const POLICY_ARTIFACT_EMPTY_UNEXISTING_LABELS = Object.freeze({ ), emptyUnexistingImportButtonTitle: i18n.translate( 'xpack.securitySolution.endpoint.policy.artifacts.empty.unexisting.importAction', - { defaultMessage: 'Import artifact list' } + { defaultMessage: 'Import artifacts' } ), }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/blocklists_translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/blocklists_translations.ts index fda35e93120a9..85fc316f4c7d5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/blocklists_translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/blocklists_translations.ts @@ -132,7 +132,7 @@ export const POLICY_ARTIFACT_BLOCKLISTS_LABELS: Omit< ), emptyUnexistingImportButtonTitle: i18n.translate( 'xpack.securitySolution.endpoint.policy.blocklist.empty.unexisting.importAction', - { defaultMessage: 'Import blocklist' } + { defaultMessage: 'Import blocklist entries' } ), listTotalItemCountMessage: (totalItemsCount: number): string => i18n.translate('xpack.securitySolution.endpoint.policy.blocklists.list.totalItemCount', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/endpoint_exceptions_translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/endpoint_exceptions_translations.ts index f96faf61aadf0..eacae23120474 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/endpoint_exceptions_translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/endpoint_exceptions_translations.ts @@ -138,7 +138,7 @@ export const POLICY_ARTIFACT_ENDPOINT_EXCEPTIONS_LABELS: Omit< ), emptyUnexistingImportButtonTitle: i18n.translate( 'xpack.securitySolution.endpoint.policy.endpointExceptions.empty.unexisting.importAction', - { defaultMessage: 'Import endpoint exception list' } + { defaultMessage: 'Import Endpoint exceptions' } ), listTotalItemCountMessage: (totalItemsCount: number): string => i18n.translate( diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/event_filters_translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/event_filters_translations.ts index be38abfda3a32..c331448b5ad9b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/event_filters_translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/event_filters_translations.ts @@ -130,7 +130,7 @@ export const POLICY_ARTIFACT_EVENT_FILTERS_LABELS: Omit< ), emptyUnexistingImportButtonTitle: i18n.translate( 'xpack.securitySolution.endpoint.policy.eventFilters.empty.unexisting.importAction', - { defaultMessage: 'Import event filter list' } + { defaultMessage: 'Import event filters' } ), listTotalItemCountMessage: (totalItemsCount: number): string => i18n.translate('xpack.securitySolution.endpoint.policy.eventFilters.list.totalItemCount', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/host_isolation_exceptions_translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/host_isolation_exceptions_translations.ts index b59411991bd6d..438096a6ab23a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/host_isolation_exceptions_translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/host_isolation_exceptions_translations.ts @@ -139,7 +139,7 @@ export const POLICY_ARTIFACT_HOST_ISOLATION_EXCEPTIONS_LABELS: Omit< ), emptyUnexistingImportButtonTitle: i18n.translate( 'xpack.securitySolution.endpoint.policy.hostIsolationException.empty.unexisting.importAction', - { defaultMessage: 'Import host isolation exception list' } + { defaultMessage: 'Import host isolation exceptions' } ), listTotalItemCountMessage: (totalItemsCount: number): string => i18n.translate( diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/trusted_apps_translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/trusted_apps_translations.ts index b20dbb42b2256..6fda907fed1e1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/trusted_apps_translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/trusted_apps_translations.ts @@ -130,7 +130,7 @@ export const POLICY_ARTIFACT_TRUSTED_APPS_LABELS: Omit< ), emptyUnexistingImportButtonTitle: i18n.translate( 'xpack.securitySolution.endpoint.policy.trustedApps.empty.unexisting.importAction', - { defaultMessage: 'Import trusted application list' } + { defaultMessage: 'Import trusted applications' } ), listTotalItemCountMessage: (totalItemsCount: number): string => i18n.translate('xpack.securitySolution.endpoint.policy.trustedApps.list.totalItemCount', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/trusted_devices_translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/trusted_devices_translations.ts index 6916725de9741..5b0fe484e35dd 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/trusted_devices_translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/trusted_devices_translations.ts @@ -133,7 +133,7 @@ export const POLICY_ARTIFACT_TRUSTED_DEVICES_LABELS: Omit< ), emptyUnexistingImportButtonTitle: i18n.translate( 'xpack.securitySolution.endpoint.policy.trustedDevices.empty.unexisting.importAction', - { defaultMessage: 'Import trusted device list' } + { defaultMessage: 'Import trusted devices' } ), listTotalItemCountMessage: (totalItemsCount: number): string => i18n.translate('xpack.securitySolution.endpoint.policy.trustedDevices.list.totalItemCount', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx index fe6fd2174184e..9f03a146c4490 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx @@ -39,37 +39,31 @@ const TRUSTED_APPS_PAGE_LABELS: ArtifactListPageLabels = { pageImportButtonTitle: i18n.translate( 'xpack.securitySolution.trustedApps.pageImportButtonTitle', { - defaultMessage: 'Import trusted application list', + defaultMessage: 'Import trusted applications', } ), pageExportButtonTitle: i18n.translate( 'xpack.securitySolution.trustedApps.pageExportButtonTitle', { - defaultMessage: 'Export trusted application list', + defaultMessage: 'Export trusted applications', } ), pageExportSuccessToastTitle: i18n.translate( 'xpack.securitySolution.trustedApps.pageExportSuccessToastTitle', { - defaultMessage: 'Trusted application list exported successfully', + defaultMessage: 'Trusted applications exported successfully', } ), pageExportErrorToastTitle: i18n.translate( 'xpack.securitySolution.trustedApps.pageExportErrorToastTitle', { - defaultMessage: 'Trusted application list export failed', + defaultMessage: 'Trusted applications export failed', } ), - pageImportSuccessToastTitle: i18n.translate( - 'xpack.securitySolution.trustedApps.pageImportSuccessToastTitle', + pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( + 'xpack.securitySolution.trustedApps.pageImportOnlyCurrentArtifactCanBeImportedError', { - defaultMessage: 'Trusted application list imported successfully', - } - ), - pageImportErrorToastTitle: i18n.translate( - 'xpack.securitySolution.trustedApps.pageImportErrorToastTitle', - { - defaultMessage: 'Trusted application list import failed', + defaultMessage: 'You can only import trusted applications here.', } ), getShowingCountLabel: (total) => @@ -149,7 +143,7 @@ const TRUSTED_APPS_PAGE_LABELS: ArtifactListPageLabels = { ), emptyStateImportButtonLabel: i18n.translate( 'xpack.securitySolution.trustedApps.emptyStateImportButtonLabel', - { defaultMessage: 'Import trusted application list' } + { defaultMessage: 'Import trusted applications' } ), searchPlaceholderInfo: i18n.translate( 'xpack.securitySolution.trustedApps.searchPlaceholderInfo', diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/trusted_devices_list.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/trusted_devices_list.tsx index dbcd38a1c58b6..dfb087159d0af 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/trusted_devices_list.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/trusted_devices_list.tsx @@ -45,37 +45,31 @@ const TRUSTED_DEVICES_PAGE_LABELS: ArtifactListPageLabels = { pageImportButtonTitle: i18n.translate( 'xpack.securitySolution.trustedDevices.list.pageImportButtonTitle', { - defaultMessage: 'Import trusted device list', + defaultMessage: 'Import trusted devices', } ), pageExportButtonTitle: i18n.translate( 'xpack.securitySolution.trustedDevices.list.pageExportButtonTitle', { - defaultMessage: 'Export trusted device list', + defaultMessage: 'Export trusted devices', } ), pageExportSuccessToastTitle: i18n.translate( 'xpack.securitySolution.trustedDevices.list.pageExportSuccessToastTitle', { - defaultMessage: 'Trusted device list exported successfully', + defaultMessage: 'Trusted devices exported successfully', } ), pageExportErrorToastTitle: i18n.translate( 'xpack.securitySolution.trustedDevices.list.pageExportErrorToastTitle', { - defaultMessage: 'Trusted device list export failed', + defaultMessage: 'Trusted devices export failed', } ), - pageImportSuccessToastTitle: i18n.translate( - 'xpack.securitySolution.trustedDevices.list.pageImportSuccessToastTitle', + pageImportOnlyCurrentArtifactCanBeImportedError: i18n.translate( + 'xpack.securitySolution.trustedDevices.list.pageImportOnlyCurrentArtifactCanBeImportedError', { - defaultMessage: 'Trusted device list imported successfully', - } - ), - pageImportErrorToastTitle: i18n.translate( - 'xpack.securitySolution.trustedDevices.list.pageImportErrorToastTitle', - { - defaultMessage: 'Trusted device list import failed', + defaultMessage: 'You can only import trusted devices here.', } ), getShowingCountLabel: (total) => @@ -165,7 +159,7 @@ const TRUSTED_DEVICES_PAGE_LABELS: ArtifactListPageLabels = { ), emptyStateImportButtonLabel: i18n.translate( 'xpack.securitySolution.trustedDevices.list.emptyStateImportButtonLabel', - { defaultMessage: 'Import trusted device list' } + { defaultMessage: 'Import trusted devices' } ), searchPlaceholderInfo: i18n.translate( 'xpack.securitySolution.trustedDevices.list.searchPlaceholderInfo', diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts b/x-pack/solutions/security/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts index b5a5a75294fdf..4e19f54035930 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts @@ -275,7 +275,8 @@ describe('Exceptions List Api Client', () => { headers: { 'Content-Type': undefined }, body: expect.any(FormData), query: { - overwrite: true, + overwrite: false, + as_new_list: false, }, }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts b/x-pack/solutions/security/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts index f8cb56e4e8544..728f971742787 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts @@ -319,7 +319,8 @@ export class ExceptionsListApiClient { body: formData, headers: { 'Content-Type': undefined }, query: { - overwrite: true, + overwrite: false, + as_new_list: false, } as ImportExceptionListRequestQuery, }); } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.test.ts index 5a12b473efe89..b948b8d84369e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.test.ts @@ -654,6 +654,28 @@ describe('When using Artifacts Exceptions BaseValidator', () => { }); }); + describe('#validateCanImportGlobalArtifacts()', () => { + beforeEach(() => { + exceptionLikeItem.tags = [GLOBAL_ARTIFACT_TAG]; + }); + + it('should error is user does not have new global artifact management privilege', async () => { + authzMock.canManageGlobalArtifacts = false; + + await expect( + validator._validateCanImportGlobalArtifacts(exceptionLikeItem) + ).rejects.toThrow( + /This artifact can't be imported because you don't have permission to manage global artifacts./ + ); + }); + + it('should allow import of global artifacts when user has privilege', async () => { + await expect( + validator._validateCanImportGlobalArtifacts(exceptionLikeItem) + ).resolves.toBeUndefined(); + }); + }); + describe('#validateImportOwnerSpaceIds()', () => { it('should do nothing when item has no tags', async () => { exceptionLikeItem.tags = []; @@ -676,7 +698,7 @@ describe('When using Artifacts Exceptions BaseValidator', () => { setArtifactOwnerSpaceId(exceptionLikeItem, 'inaccessible-space'); await expect(validator._validateImportOwnerSpaceIds(exceptionLikeItem)).rejects.toThrow( - /invalid owner space IDs/ + /This artifact can\'t be imported because it belongs to a space you don\'t have access to/ ); }); @@ -689,7 +711,7 @@ describe('When using Artifacts Exceptions BaseValidator', () => { ]); await expect(validator._validateImportOwnerSpaceIds(exceptionLikeItem)).rejects.toThrow( - /Importing artifacts that are not visible in the current space/ + /This artifact can't be imported because it isn't visible in the current space/ ); }); }); @@ -709,7 +731,7 @@ describe('When using Artifacts Exceptions BaseValidator', () => { setArtifactOwnerSpaceId(exceptionLikeItem, 'other-space'); await expect(validator._validateImportOwnerSpaceIds(exceptionLikeItem)).rejects.toThrow( - /Importing artifacts to a different space requires global artifact management privilege/ + /This artifact can't be imported because you don't have permission to manage artifacts in other spaces/ ); }); @@ -719,7 +741,7 @@ describe('When using Artifacts Exceptions BaseValidator', () => { setArtifactOwnerSpaceId(exceptionLikeItem, 'other-space'); await expect(validator._validateImportOwnerSpaceIds(exceptionLikeItem)).rejects.toThrow( - /Importing artifacts to a different space requires global artifact management privilege/ + /This artifact can't be imported because you don't have permission to manage artifacts in other spaces/ ); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.ts b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.ts index 472fc2853ce8e..9bea0be5b111d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.ts @@ -47,7 +47,15 @@ const IMPORTING_TO_OTHER_SPACE_NOT_ALLOWED_MESSAGE = i18n.translate( 'xpack.securitySolution.baseValidator.importingToOtherSpaceNotAllowedMessage', { defaultMessage: - 'Importing artifacts to a different space requires global artifact management privilege', + "This artifact can't be imported because you don't have permission to manage artifacts in other spaces. Contact your administrator for access.", + } +); + +const GLOBAL_ARTIFACT_IMPORT_NOT_ALLOWED_MESSAGE = i18n.translate( + 'xpack.securitySolution.baseValidator.noGlobalArtifactImportMessage', + { + defaultMessage: + "This artifact can't be imported because you don't have permission to manage global artifacts. Contact your administrator for access.", } ); @@ -62,19 +70,18 @@ export const GLOBAL_ARTIFACT_MANAGEMENT_NOT_ALLOWED_MESSAGE = i18n.translate( const IMPORTING_ARTIFACT_NOT_VISIBLE_IN_CURRENT_SPACE_NOT_ALLOWED_MESSAGE = i18n.translate( 'xpack.securitySolution.baseValidator.importingArtifactNotVisibleInCurrentSpace', { - defaultMessage: 'Importing artifacts that are not visible in the current space is not allowed', + defaultMessage: + "This artifact can't be imported because it isn't visible in the current space. Try importing it from a matching space or a space with access to the related policy.", } ); -const IMPORTING_ARTIFACT_WITH_INVALID_OWNER_SPACE_ID = (spaceIds: string[]): string => - i18n.translate('xpack.securitySolution.baseValidator.invalidOwnerSpaceId', { +const IMPORTING_ARTIFACT_WITH_INVALID_OWNER_SPACE_ID = i18n.translate( + 'xpack.securitySolution.baseValidator.invalidOwnerSpaceId', + { defaultMessage: - 'Importing artifacts with invalid owner space IDs is not allowed. The following space {numberOfSpaces, plural, one {ID is} other {IDs are} } invalid or unaccessible by current user: {invalidSpaceIds}', - values: { - invalidSpaceIds: spaceIds.join(', '), - numberOfSpaces: spaceIds.length, - }, - }); + "This artifact can't be imported because it belongs to a space you don't have access to. Update the artifact in its original space and try again.", + } +); const ITEM_CANNOT_BE_MANAGED_IN_CURRENT_SPACE_MESSAGE = (spaceIds: string[]): string => i18n.translate('xpack.securitySolution.baseValidator.cannotManageItemInCurrentSpace', { @@ -342,7 +349,7 @@ export class BaseValidator { if (invalidSpaceIds.length > 0) { throw new EndpointArtifactExceptionValidationError( - IMPORTING_ARTIFACT_WITH_INVALID_OWNER_SPACE_ID(invalidSpaceIds), + IMPORTING_ARTIFACT_WITH_INVALID_OWNER_SPACE_ID, 403 ); } @@ -422,6 +429,15 @@ export class BaseValidator { } } + protected async validateCanImportGlobalArtifacts(item: ExceptionItemLikeOptions): Promise { + if (!this.isItemByPolicy(item) && !(await this.endpointAuthzPromise).canManageGlobalArtifacts) { + throw new EndpointArtifactExceptionValidationError( + `${ENDPOINT_AUTHZ_ERROR_MESSAGE}. ${GLOBAL_ARTIFACT_IMPORT_NOT_ALLOWED_MESSAGE}`, + 403 + ); + } + } + protected async validateCanUpdateItemInActiveSpace( updatedItem: Partial>, currentSavedItem: ExceptionListItemSchema diff --git a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/blocklist_validator.ts b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/blocklist_validator.ts index 9e634b46f7c5e..607c48e0257b0 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/blocklist_validator.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/blocklist_validator.ts @@ -237,7 +237,7 @@ export class BlocklistValidator extends BaseValidator { await this.validatePreImportItems(items, async (item) => { // import specific validations await this.validateImportOwnerSpaceIds(item); // instead of validateCreateOwnerSpaceIds - await this.validateCanCreateGlobalArtifacts(item); + await this.validateCanImportGlobalArtifacts(item); // instead of validateCanCreateGlobalArtifacts await this.removeInvalidPolicyIds(item); // instead of validateByPolicyItem // usual validators from pre-create diff --git a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/endpoint_exceptions_validator.ts b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/endpoint_exceptions_validator.ts index 76248351499e7..6f4c113f48bc2 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/endpoint_exceptions_validator.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/endpoint_exceptions_validator.ts @@ -49,7 +49,7 @@ export class EndpointExceptionsValidator extends BaseValidator { await this.validatePreImportItems(items, async (item) => { // import specific validations await this.validateImportOwnerSpaceIds(item); // instead of validateCreateOwnerSpaceIds - await this.validateCanCreateGlobalArtifacts(item); + await this.validateCanImportGlobalArtifacts(item); // instead of validateCanCreateGlobalArtifacts await this.removeInvalidPolicyIds(item); // instead of validateByPolicyItem // usual validators from pre-create diff --git a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/event_filter_validator.ts b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/event_filter_validator.ts index 271209f458d37..e98c7055dba35 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/event_filter_validator.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/event_filter_validator.ts @@ -56,7 +56,7 @@ export class EventFilterValidator extends BaseValidator { await this.validatePreImportItems(items, async (item) => { // import specific validations await this.validateImportOwnerSpaceIds(item); // instead of validateCreateOwnerSpaceIds - await this.validateCanCreateGlobalArtifacts(item); + await this.validateCanImportGlobalArtifacts(item); // instead of validateCanCreateGlobalArtifacts await this.removeInvalidPolicyIds(item); // instead of validateByPolicyItem // usual validators from pre-create diff --git a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/host_isolation_exceptions_validator.ts b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/host_isolation_exceptions_validator.ts index c2a272400d8db..25bd178bc85d7 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/host_isolation_exceptions_validator.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/host_isolation_exceptions_validator.ts @@ -80,7 +80,7 @@ export class HostIsolationExceptionsValidator extends BaseValidator { await this.validatePreImportItems(items, async (item) => { // import specific validations await this.validateImportOwnerSpaceIds(item); // instead of validateCreateOwnerSpaceIds - await this.validateCanCreateGlobalArtifacts(item); + await this.validateCanImportGlobalArtifacts(item); // instead of validateCanCreateGlobalArtifacts await this.removeInvalidPolicyIds(item); // instead of validateByPolicyItem // usual validators from pre-create diff --git a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/mocks.ts b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/mocks.ts index 7025f7b478fa4..28c6ca11c539a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/mocks.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/mocks.ts @@ -70,6 +70,10 @@ export class BaseValidatorMock extends BaseValidator { return this.validateCanCreateGlobalArtifacts(item); } + _validateCanImportGlobalArtifacts(item: ExceptionItemLikeOptions): Promise { + return this.validateCanImportGlobalArtifacts(item); + } + _validateCanUpdateItemInActiveSpace( updatedItem: Partial>, currentSavedItem: ExceptionListItemSchema diff --git a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts index 00d72ec85d5ef..42ec751483c27 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts @@ -227,7 +227,7 @@ export class TrustedAppValidator extends BaseValidator { await this.validatePreImportItems(items, async (item) => { // import specific validations await this.validateImportOwnerSpaceIds(item); // instead of validateCreateOwnerSpaceIds - await this.validateCanCreateGlobalArtifacts(item); + await this.validateCanImportGlobalArtifacts(item); // instead of validateCanCreateGlobalArtifacts await this.removeInvalidPolicyIds(item); // instead of validateByPolicyItem // usual validators from pre-create diff --git a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_device_validator.ts b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_device_validator.ts index 7fe07fe66ce11..c651a7a8158c9 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_device_validator.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_device_validator.ts @@ -127,7 +127,7 @@ export class TrustedDeviceValidator extends BaseValidator { await this.validatePreImportItems(items, async (item) => { // import specific validations await this.validateImportOwnerSpaceIds(item); // instead of validateCreateOwnerSpaceIds - await this.validateCanCreateGlobalArtifacts(item); + await this.validateCanImportGlobalArtifacts(item); // instead of validateCanCreateGlobalArtifacts await this.removeInvalidPolicyIds(item); // instead of validateByPolicyItem // usual validators from pre-create diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/artifact_import.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/artifact_import.ts index bf36e7c5b2593..b3168b026fde8 100644 --- a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/artifact_import.ts +++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/artifact_import.ts @@ -327,7 +327,7 @@ export default function artifactImportAPIIntegrationTests({ getService }: FtrPro { error: { message: - 'EndpointArtifactError: Endpoint authorization failure. Importing artifacts to a different space requires global artifact management privilege', + "EndpointArtifactError: Endpoint authorization failure. This artifact can't be imported because you don't have permission to manage artifacts in other spaces. Contact your administrator for access.", status_code: 403, }, item_id: 'wrong-item', @@ -373,7 +373,7 @@ export default function artifactImportAPIIntegrationTests({ getService }: FtrPro { error: { message: - 'EndpointArtifactError: Endpoint authorization failure. Management of global artifacts requires additional privilege (global artifact management)', + "EndpointArtifactError: Endpoint authorization failure. This artifact can't be imported because you don't have permission to manage global artifacts. Contact your administrator for access.", status_code: 403, }, item_id: 'wrong-item', @@ -498,7 +498,7 @@ export default function artifactImportAPIIntegrationTests({ getService }: FtrPro { error: { message: - 'EndpointArtifactError: Endpoint authorization failure. Importing artifacts that are not visible in the current space is not allowed', + "EndpointArtifactError: Endpoint authorization failure. This artifact can't be imported because it isn't visible in the current space. Try importing it from a matching space or a space with access to the related policy.", status_code: 403, }, item_id: 'not-visible-in-current-space-because-unassigned', @@ -507,7 +507,7 @@ export default function artifactImportAPIIntegrationTests({ getService }: FtrPro { error: { message: - 'EndpointArtifactError: Endpoint authorization failure. Importing artifacts that are not visible in the current space is not allowed', + "EndpointArtifactError: Endpoint authorization failure. This artifact can't be imported because it isn't visible in the current space. Try importing it from a matching space or a space with access to the related policy.", status_code: 403, }, item_id: 'not-visible-in-current-space-because-assigned-only-there', @@ -555,7 +555,7 @@ export default function artifactImportAPIIntegrationTests({ getService }: FtrPro { error: { message: - 'EndpointArtifactError: Endpoint authorization failure. Importing artifacts to a different space requires global artifact management privilege', + "EndpointArtifactError: Endpoint authorization failure. This artifact can't be imported because you don't have permission to manage artifacts in other spaces. Contact your administrator for access.", status_code: 403, }, item_id: 'global-artifact-with-invalid-space-id', @@ -564,7 +564,7 @@ export default function artifactImportAPIIntegrationTests({ getService }: FtrPro { error: { message: - 'EndpointArtifactError: Endpoint authorization failure. Importing artifacts to a different space requires global artifact management privilege', + "EndpointArtifactError: Endpoint authorization failure. This artifact can't be imported because you don't have permission to manage artifacts in other spaces. Contact your administrator for access.", status_code: 403, }, item_id: 'per-policy-artifact-with-invalid-space-id', @@ -611,7 +611,7 @@ export default function artifactImportAPIIntegrationTests({ getService }: FtrPro { error: { message: - 'EndpointArtifactError: Importing artifacts with invalid owner space IDs is not allowed. The following space ID is invalid or unaccessible by current user: i-dont-exist-1', + "EndpointArtifactError: This artifact can't be imported because it belongs to a space you don't have access to. Update the artifact in its original space and try again.", status_code: 403, }, item_id: 'global-artifact-with-invalid-space-id', @@ -620,7 +620,7 @@ export default function artifactImportAPIIntegrationTests({ getService }: FtrPro { error: { message: - 'EndpointArtifactError: Importing artifacts with invalid owner space IDs is not allowed. The following space IDs are invalid or unaccessible by current user: i-dont-exist-2, i-dont-exist-3', + "EndpointArtifactError: This artifact can't be imported because it belongs to a space you don't have access to. Update the artifact in its original space and try again.", status_code: 403, }, item_id: 'per-policy-artifact-with-invalid-space-id', @@ -1350,7 +1350,7 @@ export default function artifactImportAPIIntegrationTests({ getService }: FtrPro { error: { message: - 'EndpointArtifactError: Endpoint authorization failure. Management of global artifacts requires additional privilege (global artifact management)', + "EndpointArtifactError: Endpoint authorization failure. This artifact can't be imported because you don't have permission to manage global artifacts. Contact your administrator for access.", status_code: 403, }, item_id: 'imported-artifact',