Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
090c642
[ui] add import and export buttons
gergoabraham Jan 6, 2026
6d5f36f
[ui] add artifact export functionality
gergoabraham Jan 6, 2026
988a556
[ui] add import flyout
gergoabraham Jan 6, 2026
63304aa
[ui] a bit less verbose import button
gergoabraham Dec 3, 2025
88ea82f
[ui] add Import button on empty list screen
gergoabraham Jan 6, 2026
e25a1aa
[ui] display Import flyout in the url
gergoabraham Jan 6, 2026
8f30d8c
[ui] add Import button to policy details artifact tab empty pages
gergoabraham Jan 6, 2026
e523b97
[api/validator] allow importing endpoint artifacts
gergoabraham Jan 7, 2026
5581382
[api/test] extract generateItem into generator
gergoabraham Jan 7, 2026
af0547d
[api/test] add initial api test for import
gergoabraham Jan 7, 2026
6f6fb0b
[api/test] move existing import tests to new suite
gergoabraham Jan 7, 2026
d81ba1c
[api/validator] unify error message across artifacts
gergoabraham Dec 5, 2025
6d6969c
[api/validator] tie import to ALL privilege
gergoabraham Dec 5, 2025
a5f4818
[chore] make tests run on serverless with custom roles
gergoabraham Dec 5, 2025
dc61ea6
[api/validator] add policy:all only with missing spaceOwnerId
gergoabraham Dec 8, 2025
bb2b88d
[api/lists] allow passing extra error info to import API
gergoabraham Dec 8, 2025
75519ca
[api/validator] tie to global artifact privilege importing to other s…
gergoabraham Dec 8, 2025
1b4d88c
[api/validator] tie to global artifact privilege importing global art…
gergoabraham Dec 8, 2025
64e69cd
[api/validator] remove invalid policy ids
gergoabraham Dec 9, 2025
d300d39
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,26 @@ export const ArtifactListPage = memo<ArtifactListPageProps>(
allowCardDeleteAction = true,
CardDecorator,
}) => {
const areEndpointExceptionsMovedUnderManagementFFEnabled = 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) &&
areEndpointExceptionsMovedUnderManagementFFEnabled;

const setUrlParams = useSetUrlParams();
const {
urlParams: { filter, includedPolicies },
} = useUrlParams<ArtifactListPageUrlParams>();
const { exportExceptionList } = useApi(http);

const {
isPageInitializing,
Expand Down Expand Up @@ -130,6 +147,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 +256,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 +321,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>
)}

{areEndpointExceptionsMovedUnderManagementFFEnabled && (
<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 +380,15 @@ export const ArtifactListPage = memo<ArtifactListPageProps>(
/>
)}

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

{selectedItemForDelete && (
<ArtifactDeleteModal
apiClient={apiClient}
Expand All @@ -308,10 +403,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