Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ade7a2f
[chore] add feature flag
gergoabraham Dec 1, 2025
0e2f450
[ui] add import and export buttons
gergoabraham Dec 1, 2025
25fac29
[ui] add artifact export functionality
gergoabraham Dec 1, 2025
c7dd9b3
[ui] add import flyout
gergoabraham Dec 2, 2025
75b2b76
[api/validator] allow importing endpoint artifacts
gergoabraham Dec 3, 2025
2e09870
[api/test] refactor
gergoabraham Dec 3, 2025
9b91ce8
[api/test] add initial api test for import
gergoabraham Dec 3, 2025
0b30ba8
[ui] a bit less verbose import button
gergoabraham Dec 3, 2025
59a4398
[ui] add Import button on empty list screen
gergoabraham Dec 3, 2025
35e9cef
[ui] display Import flyout in the url
gergoabraham Dec 3, 2025
fa099b3
[ui] add Import button to policy details artifact tab empty pages
gergoabraham Dec 3, 2025
f46c3b9
[chore] simplification: use existing FF instead of a separate one
gergoabraham Dec 4, 2025
1e511a6
[chore] simplification: remove separate test config for import, run i…
gergoabraham Dec 4, 2025
380fe94
[chore] simplification: move ff-less import tests to new suite
gergoabraham Dec 4, 2025
f313fde
[api/validator] unify error message across artifacts
gergoabraham Dec 5, 2025
2b35950
[api/validator] tie import to ALL privilege
gergoabraham Dec 5, 2025
1cababd
[chore] make tests run on serverless with custom roles
gergoabraham Dec 5, 2025
1079d18
[api/validator] add policy:all only with missing spaceOwnerId
gergoabraham Dec 8, 2025
6625d04
[api/lists] allow passing extra error info to import API
gergoabraham Dec 8, 2025
b93bda7
[api/validator] tie to global artifact privilege importing to other s…
gergoabraham Dec 8, 2025
c8270ef
[api/validator] tie to global artifact privilege importing global art…
gergoabraham Dec 8, 2025
68de7ab
[api/validator] remove invalid policy ids
gergoabraham Dec 9, 2025
75b10c4
wip
gergoabraham Dec 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export const bulkErrorErrorSchema = t.exact(
})
);

export type BulkErrorErrorSchema = t.TypeOf<typeof bulkErrorErrorSchema>;

export const bulkErrorSchema = t.intersection([
t.exact(
t.type({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* 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 type { BulkErrorErrorSchema } from '@kbn/securitysolution-io-ts-list-types';

import { ListsErrorWithStatusCode } from '.';

export class ExceptionItemImportError extends Error implements BulkErrorErrorSchema {
public readonly status_code: number;

constructor(error: Error, public readonly listId: string, public readonly itemId: string) {
super(error.message);
this.status_code = error instanceof ListsErrorWithStatusCode ? error.getStatusCode() : 400;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1146,6 +1146,10 @@ export class ExceptionListClient {
overwrite,
generateNewListId,
}: ImportExceptionListAndItemsOptions): Promise<ImportExceptionsResponseSchema> => {
console.log(
'🧀 exception_list_client.ts:1086 🥭 importExceptionListAndItems',
JSON.stringify({}, null, ' ')
);
const { savedObjectsClient, user } = this;

// validation of import and sorting of lists and items
Expand Down Expand Up @@ -1185,6 +1189,10 @@ export class ExceptionListClient {
maxExceptionsImportSize,
overwrite,
}: ImportExceptionListAndItemsAsArrayOptions): Promise<ImportExceptionsResponseSchema> => {
console.log(
'🧀 exception_list_client.ts:1126 🥭 importExceptionListAndItemsAsArray',
JSON.stringify({}, null, ' ')
);
const { savedObjectsClient, user } = this;

// validation of import and sorting of lists and items
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
*/

import { v4 as uuidv4 } from 'uuid';
import type {
BulkErrorSchema,
ImportExceptionListItemSchemaDecoded,
import {
type BulkErrorSchema,
type ImportExceptionListItemSchemaDecoded,
} from '@kbn/securitysolution-io-ts-list-types';

import { ExceptionItemImportError } from '../../../../exception_item_import_error';

/**
* Reports on duplicates and returns unique array of items
* @param items - exception items to be checked for duplicate list_ids
Expand All @@ -21,7 +23,14 @@ export const getTupleErrorsAndUniqueExceptionListItems = (
): [BulkErrorSchema[], ImportExceptionListItemSchemaDecoded[]] => {
const { errors, itemsAcc } = items.reduce(
(acc, parsedExceptionItem) => {
if (parsedExceptionItem instanceof Error) {
if (parsedExceptionItem instanceof ExceptionItemImportError) {
acc.errors.set(uuidv4(), {
error: parsedExceptionItem,

item_id: parsedExceptionItem.itemId,
list_id: parsedExceptionItem.listId,
});
} else if (parsedExceptionItem instanceof Error) {
acc.errors.set(uuidv4(), {
error: {
message: `Error found importing exception list item: ${parsedExceptionItem.message}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ListOperatorTypeEnum,
type ListOperatorType,
} from '@kbn/securitysolution-io-ts-list-types';
import type { ENDPOINT_ARTIFACT_LIST_IDS } from '@kbn/securitysolution-list-constants';
import { ENDPOINT_ARTIFACT_LISTS, ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants';
import { ConditionEntryField } from '@kbn/securitysolution-utils';
import { LIST_ITEM_ENTRY_OPERATOR_TYPES } from './common/artifact_list_item_entry_values';
Expand Down Expand Up @@ -453,4 +454,32 @@ export class ExceptionsListItemGenerator extends BaseDataGenerator<ExceptionList
...overrides,
};
}

generateEndpointArtifact = (
listId: (typeof ENDPOINT_ARTIFACT_LIST_IDS)[number],
overrides: Partial<ExceptionListItemSchema> = {}
) => {
switch (listId) {
case ENDPOINT_ARTIFACT_LISTS.endpointExceptions.id:
return this.generateEndpointException(overrides);

case ENDPOINT_ARTIFACT_LISTS.blocklists.id:
return this.generateBlocklist(overrides);

case ENDPOINT_ARTIFACT_LISTS.eventFilters.id:
return this.generateEventFilter(overrides);

case ENDPOINT_ARTIFACT_LISTS.hostIsolationExceptions.id:
return this.generateHostIsolationException(overrides);

case ENDPOINT_ARTIFACT_LISTS.trustedApps.id:
return this.generateTrustedApp(overrides);

case ENDPOINT_ARTIFACT_LISTS.trustedDevices.id:
return this.generateTrustedDevice(overrides);

default:
throw new Error(`Unknown listId: ${listId}. Unable to generate exception list item.`);
}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@
import React, { memo, useCallback, useMemo, useState, useEffect } from 'react';

import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui';
import { EuiButton, EuiFlexGroup, EuiSpacer, EuiText } from '@elastic/eui';
import type { EuiFlyoutSize } from '@elastic/eui/src/components/flyout/flyout';
import { useLocation } from 'react-router-dom';
import { useIsMounted } from '@kbn/securitysolution-hook-utils';
import { HeaderMenu } from '@kbn/securitysolution-exception-list-components';
import { useApi } from '@kbn/securitysolution-list-hooks';
import { AutoDownload } from '../../../common/components/auto_download/auto_download';
import type { ServerApiError } from '../../../common/types';
import { AdministrationListPage } from '../administration_list_page';

Expand Down Expand Up @@ -41,10 +44,13 @@ import { useUrlParams } from '../../hooks/use_url_params';
import type { ListPageRouteState, MaybeImmutable } from '../../../../common/endpoint/types';
import { DEFAULT_EXCEPTION_LIST_ITEM_SEARCHABLE_FIELDS } from '../../../../common/endpoint/service/artifacts/constants';
import { ArtifactDeleteModal } from './components/artifact_delete_modal';
import { useToasts } from '../../../common/lib/kibana';
import { useKibana, useToasts } from '../../../common/lib/kibana';
import { useMemoizedRouteState } from '../../common/hooks';
import { BackToExternalAppSecondaryButton } from '../back_to_external_app_secondary_button';
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';

type ArtifactEntryCardType = typeof ArtifactEntryCard;

Expand Down Expand Up @@ -92,15 +98,24 @@ export const ArtifactListPage = memo<ArtifactListPageProps>(
allowCardDeleteAction = true,
CardDecorator,
}) => {
const isEndpointExceptionsMovedUnderManagementFFEnabled = useIsExperimentalFeatureEnabled(
'endpointExceptionsMovedUnderManagement'
);
const { services } = useKibana();
const { http } = services;
const { state: routeState } = useLocation<ListPageRouteState | undefined>();
const getTestId = useTestIdGenerator(dataTestSubj);
const toasts = useToasts();
const isMounted = useIsMounted();
const isFlyoutOpened = useIsFlyoutOpened(allowCardEditAction, allowCardCreateAction);
const isImportFlyoutOpened =
useIsImportFlyoutOpened(allowCardCreateAction) &&
isEndpointExceptionsMovedUnderManagementFFEnabled;
const setUrlParams = useSetUrlParams();
const {
urlParams: { filter, includedPolicies },
} = useUrlParams<ArtifactListPageUrlParams>();
const { exportExceptionList } = useApi(http);

const {
isPageInitializing,
Expand Down Expand Up @@ -130,6 +145,8 @@ export const ArtifactListPage = memo<ArtifactListPageProps>(
undefined | ExceptionListItemSchema
>(undefined);

const [exportedData, setExportedData] = useState<Blob>();

const labels = useMemo<typeof artifactListPageLabels>(() => {
return {
...artifactListPageLabels,
Expand Down Expand Up @@ -237,6 +254,42 @@ export const ArtifactListPage = memo<ArtifactListPageProps>(
setSelectedItemForEdit(undefined);
}, []);

const handleExport = useCallback(
() =>
exportExceptionList({
id: apiClient.listId,
listId: apiClient.listId,
includeExpiredExceptions: true,
namespaceType: 'agnostic',

onError: (exportError: Error) =>
toasts?.addError(exportError, { title: labels.pageExportErrorToastTitle }),

onSuccess: (blob) => {
setExportedData(blob);
toasts?.addSuccess(labels.pageExportSuccessToastTitle);
},
}),
[
exportExceptionList,
apiClient.listId,
toasts,
labels.pageExportErrorToastTitle,
labels.pageExportSuccessToastTitle,
]
);

const handleOnDownload = useCallback(() => setExportedData(undefined), []);

const handleImport = useCallback(() => setUrlParams({ show: 'import' }), [setUrlParams]);

const closeImportFlyout = useCallback(() => setUrlParams({ show: undefined }), [setUrlParams]);

const handleImportFlyoutOnSuccess = useCallback(() => {
closeImportFlyout();
refetchListData();
}, [closeImportFlyout, refetchListData]);

const description = useMemo(() => {
const subtitleText = labels.pageAboutInfo ? (
<span data-test-subj="header-panel-subtitle">{labels.pageAboutInfo}</span>
Expand Down Expand Up @@ -266,20 +319,51 @@ export const ArtifactListPage = memo<ArtifactListPageProps>(
title={labels.pageTitle}
subtitle={description}
actions={
allowCardCreateAction && (
<EuiButton
fill
iconType="plusInCircle"
isDisabled={isFlyoutOpened}
onClick={handleOpenCreateFlyoutClick}
data-test-subj={getTestId('pageAddButton')}
>
{labels.pageAddButtonTitle}
</EuiButton>
)
<EuiFlexGroup alignItems="center">
{allowCardCreateAction && (
<EuiButton
fill
iconType="plusInCircle"
isDisabled={isFlyoutOpened}
onClick={handleOpenCreateFlyoutClick}
data-test-subj={getTestId('pageAddButton')}
>
{labels.pageAddButtonTitle}
</EuiButton>
)}

{isEndpointExceptionsMovedUnderManagementFFEnabled && (
<HeaderMenu
iconType="boxesHorizontal"
dataTestSubj={getTestId('exportImportMenu')}
actions={[
{
key: 'ImportButton',
icon: 'importAction',
label: labels.pageImportButtonTitle,
onClick: handleImport,
disabled: !allowCardCreateAction,
},
{
key: 'ExportButton',
icon: 'exportAction',
label: labels.pageExportButtonTitle,
onClick: handleExport,
},
]}
disableActions={isLoading}
/>
)}
</EuiFlexGroup>
}
data-test-subj={getTestId('container')}
>
<AutoDownload
blob={exportedData}
name={`${apiClient.listId}.ndjson`}
onDownload={handleOnDownload}
/>

{isFlyoutOpened && (
<ArtifactFlyout
apiClient={apiClient}
Expand All @@ -294,6 +378,15 @@ export const ArtifactListPage = memo<ArtifactListPageProps>(
/>
)}

{isImportFlyoutOpened && (
<ArtifactImportFlyout
onCancel={closeImportFlyout}
onSuccess={handleImportFlyoutOnSuccess}
apiClient={apiClient}
labels={labels}
/>
)}

{selectedItemForDelete && (
<ArtifactDeleteModal
apiClient={apiClient}
Expand All @@ -308,10 +401,12 @@ export const ArtifactListPage = memo<ArtifactListPageProps>(
{!doesDataExist ? (
<NoDataEmptyState
onAdd={handleOpenCreateFlyoutClick}
onImport={handleImport}
titleNoEntriesLabel={labels.emptyStateTitleNoEntries}
titleLabel={labels.emptyStateTitle}
aboutInfo={labels.emptyStateInfo}
primaryButtonLabel={labels.emptyStatePrimaryButtonLabel}
importButtonLabel={labels.emptyStateImportButtonLabel}
backComponent={backButtonEmptyComponent}
data-test-subj={getTestId('emptyState')}
secondaryAboutInfo={secondaryPageInfo}
Expand Down
Loading