Skip to content

Commit 8e41fbf

Browse files
committed
[Security Solution][EDR] Fix import of endpoint exceptions (#233142)
## Summary - Fix import of Endpoint Exceptions to ensure they are made visible and accessible via API - A bug was introduced with `v9.1.0`, as part of support for Spaces, that made imported endpoint exceptions unaccessible after import. Items were imported into the index, but they did not include a `tag` indicating that the exception is Global. This was a new requirement with `v9.1.0` (cherry picked from commit 5be7a8f) # Conflicts: # x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_import_handler.ts # x-pack/solutions/security/test/security_solution_endpoint/services/endpoint_artifacts.ts
1 parent 80aa590 commit 8e41fbf

5 files changed

Lines changed: 222 additions & 20 deletions

File tree

x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_import_handler.ts

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,64 @@
66
*/
77

88
import type { ExceptionsListPreImportServerExtension } from '@kbn/lists-plugin/server';
9+
import { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants';
10+
import type { PromiseFromStreams } from '@kbn/lists-plugin/server/services/exception_lists/import_exception_list_and_items';
11+
import { stringify } from '../../../endpoint/utils/stringify';
12+
import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
13+
import {
14+
ALL_ENDPOINT_ARTIFACT_LIST_IDS,
15+
GLOBAL_ARTIFACT_TAG,
16+
} from '../../../../common/endpoint/service/artifacts/constants';
917
import { EndpointArtifactExceptionValidationError } from '../validators/errors';
10-
import { ALL_ENDPOINT_ARTIFACT_LIST_IDS } from '../../../../common/endpoint/service/artifacts/constants';
11-
12-
export const getExceptionsPreImportHandler =
13-
(): ExceptionsListPreImportServerExtension['callback'] => {
14-
return async ({ data }) => {
15-
const hasEndpointArtifactListOrListItems = [...data.lists, ...data.items].some((item) => {
16-
if ('list_id' in item) {
17-
return (ALL_ENDPOINT_ARTIFACT_LIST_IDS as string[]).includes(item.list_id);
18-
}
19-
20-
return false;
21-
});
22-
23-
if (hasEndpointArtifactListOrListItems) {
24-
throw new EndpointArtifactExceptionValidationError(
25-
'Import is not supported for Endpoint artifact exceptions'
26-
);
18+
19+
export const getExceptionsPreImportHandler = (
20+
endpointAppContextService: EndpointAppContextService
21+
): ExceptionsListPreImportServerExtension['callback'] => {
22+
const logger = endpointAppContextService.createLogger('listsPreImportExtensionPoint');
23+
24+
return async ({ data }) => {
25+
const hasEndpointArtifactListOrListItems = [...data.lists, ...data.items].some((item) => {
26+
if ('list_id' in item) {
27+
const NON_IMPORTABLE_ENDPOINT_ARTIFACT_IDS = ALL_ENDPOINT_ARTIFACT_LIST_IDS.filter(
28+
(listId) => listId !== ENDPOINT_ARTIFACT_LISTS.endpointExceptions.id
29+
) as string[];
30+
31+
return NON_IMPORTABLE_ENDPOINT_ARTIFACT_IDS.includes(item.list_id);
2732
}
2833

29-
return data;
30-
};
34+
return false;
35+
});
36+
37+
if (hasEndpointArtifactListOrListItems) {
38+
throw new EndpointArtifactExceptionValidationError(
39+
'Import is not supported for Endpoint artifact exceptions'
40+
);
41+
}
42+
43+
// Temporary Work-around:
44+
// v9.1.0 introduced support for spaces, which also now requires that each endpoint exception
45+
// have the `global` tag, or else they will not be returned via API. Since Endpoint
46+
// Exceptions continue to be global only in v9.1, we add the global tag to them here if it is
47+
// missing
48+
const adjustedImportItems: PromiseFromStreams['items'] = [];
49+
50+
for (const item of data.items) {
51+
if (
52+
!(item instanceof Error) &&
53+
item.list_id === ENDPOINT_ARTIFACT_LISTS.endpointExceptions.id &&
54+
item.tags?.includes(GLOBAL_ARTIFACT_TAG) === false
55+
) {
56+
item.tags = item.tags ?? [];
57+
item.tags.push(GLOBAL_ARTIFACT_TAG);
58+
adjustedImportItems.push(item);
59+
}
60+
}
61+
62+
if (adjustedImportItems.length > 0) {
63+
logger.debug(`The following Endpoint Exceptions item imports were adjusted to include the Global artifact tag:
64+
${stringify(adjustedImportItems)}`);
65+
}
66+
67+
return data;
3168
};
69+
};

x-pack/solutions/security/plugins/security_solution/server/lists_integration/endpoint/register_endpoint_extension_points.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,6 @@ export const registerListsPluginEndpointExtensionPoints = (
7272
// PRE-IMPORT
7373
registerListsExtensionPoint({
7474
type: 'exceptionsListPreImport',
75-
callback: getExceptionsPreImportHandler(),
75+
callback: getExceptionsPreImportHandler(endpointAppContextService),
7676
});
7777
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import type TestAgent from 'supertest/lib/agent';
9+
import expect from '@kbn/expect';
10+
import {
11+
ENDPOINT_ARTIFACT_LISTS,
12+
EXCEPTION_LIST_ITEM_URL,
13+
EXCEPTION_LIST_URL,
14+
} from '@kbn/securitysolution-list-constants';
15+
import {
16+
ALL_ENDPOINT_ARTIFACT_LIST_IDS,
17+
GLOBAL_ARTIFACT_TAG,
18+
} from '@kbn/security-solution-plugin/common/endpoint/service/artifacts/constants';
19+
import { ExceptionsListItemGenerator } from '@kbn/security-solution-plugin/common/endpoint/data_generators/exceptions_list_item_generator';
20+
import { ROLE } from '../../../../config/services/security_solution_edr_workflows_roles_users';
21+
import { createSupertestErrorLogger } from '../../utils';
22+
import type { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows';
23+
24+
export default function ({ getService }: FtrProviderContext) {
25+
const endpointArtifactTestResources = getService('endpointArtifactTestResources');
26+
const utils = getService('securitySolutionUtils');
27+
const log = getService('log');
28+
29+
describe('@ess @serverless Endpoint artifacts (via lists plugin): Endpoint Exceptions', function () {
30+
let endpointOpsAnalystSupertest: TestAgent;
31+
32+
before(async () => {
33+
endpointOpsAnalystSupertest = await utils.createSuperTest(ROLE.endpoint_operations_analyst);
34+
});
35+
36+
describe(`and using Import API`, function () {
37+
const buildImportBuffer = (
38+
listId: (typeof ALL_ENDPOINT_ARTIFACT_LIST_IDS)[number]
39+
): Buffer => {
40+
const generator = new ExceptionsListItemGenerator();
41+
const listInfo = Object.values(ENDPOINT_ARTIFACT_LISTS).find((listDefinition) => {
42+
return listDefinition.id === listId;
43+
});
44+
45+
if (!listInfo) {
46+
throw new Error(`Unknown listId: ${listId}. Unable to generate exception list item.`);
47+
}
48+
49+
const createItem = () => {
50+
switch (listId) {
51+
case ENDPOINT_ARTIFACT_LISTS.endpointExceptions.id:
52+
return generator.generateEndpointException();
53+
54+
case ENDPOINT_ARTIFACT_LISTS.blocklists.id:
55+
return generator.generateBlocklist();
56+
57+
case ENDPOINT_ARTIFACT_LISTS.eventFilters.id:
58+
return generator.generateEventFilter();
59+
60+
case ENDPOINT_ARTIFACT_LISTS.hostIsolationExceptions.id:
61+
return generator.generateHostIsolationException();
62+
63+
case ENDPOINT_ARTIFACT_LISTS.trustedApps.id:
64+
return generator.generateTrustedApp();
65+
66+
case ENDPOINT_ARTIFACT_LISTS.trustedDevices.id:
67+
return generator.generateTrustedDevice();
68+
69+
default:
70+
throw new Error(`Unknown listId: ${listId}. Unable to generate exception list item.`);
71+
}
72+
};
73+
74+
return Buffer.from(
75+
`
76+
{"_version":"WzEsMV0=","created_at":"2025-08-21T14:20:07.012Z","created_by":"kibana","description":"${
77+
listInfo!.description
78+
}","id":"${listId}","immutable":false,"list_id":"${listId}","name":"${
79+
listInfo!.name
80+
}","namespace_type":"agnostic","os_types":[],"tags":[],"tie_breaker_id":"034d07f4-fa33-43bb-adfa-6f6bda7921ce","type":"endpoint","updated_at":"2025-08-21T14:20:07.012Z","updated_by":"kibana","version":1}
81+
${JSON.stringify(createItem())}
82+
${JSON.stringify(createItem())}
83+
${JSON.stringify(createItem())}
84+
{"exported_exception_list_count":1,"exported_exception_list_item_count":3,"missing_exception_list_item_count":0,"missing_exception_list_items":[],"missing_exception_lists":[],"missing_exception_lists_count":0}
85+
`,
86+
'utf8'
87+
);
88+
};
89+
90+
// All non-Endpoint exceptions artifacts are not allowed to import
91+
ALL_ENDPOINT_ARTIFACT_LIST_IDS.filter(
92+
(listId) => listId !== ENDPOINT_ARTIFACT_LISTS.endpointExceptions.id
93+
).forEach((listId) => {
94+
it(`should error when importing ${listId} artifacts`, async () => {
95+
await endpointArtifactTestResources.deleteList(listId);
96+
97+
const { body } = await endpointOpsAnalystSupertest
98+
.post(`${EXCEPTION_LIST_URL}/_import`)
99+
.set('kbn-xsrf', 'true')
100+
.on('error', createSupertestErrorLogger(log).ignoreCodes([400]))
101+
.attach('file', buildImportBuffer(listId), 'import_data.ndjson')
102+
.expect(400);
103+
104+
expect(body.message).to.eql(
105+
'EndpointArtifactError: Import is not supported for Endpoint artifact exceptions'
106+
);
107+
});
108+
});
109+
110+
it('should import endpoint exceptions and add global artifact tag if missing', async () => {
111+
await endpointArtifactTestResources.deleteList(
112+
ENDPOINT_ARTIFACT_LISTS.endpointExceptions.id
113+
);
114+
115+
await endpointOpsAnalystSupertest
116+
.post(`${EXCEPTION_LIST_URL}/_import`)
117+
.set('kbn-xsrf', 'true')
118+
.on('error', createSupertestErrorLogger(log))
119+
.attach(
120+
'file',
121+
buildImportBuffer(ENDPOINT_ARTIFACT_LISTS.endpointExceptions.id),
122+
'import_exceptions.ndjson'
123+
)
124+
.expect(200);
125+
126+
const { body } = await endpointOpsAnalystSupertest
127+
.get(`${EXCEPTION_LIST_ITEM_URL}/_find`)
128+
.set('kbn-xsrf', 'true')
129+
.on('error', createSupertestErrorLogger(log))
130+
.query({
131+
list_id: 'endpoint_list',
132+
namespace_type: 'agnostic',
133+
per_page: 50,
134+
})
135+
.send()
136+
.expect(200);
137+
138+
// After import - all items should be returned on a GET `find` request.
139+
expect(body.data.length).to.eql(3);
140+
141+
for (const endpointException of body.data) {
142+
expect(endpointException.tags).to.include.string(GLOBAL_ARTIFACT_TAG);
143+
}
144+
});
145+
});
146+
});
147+
}

x-pack/solutions/security/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,6 @@ export default function endpointAPIIntegrationTests(providerContext: FtrProvider
5656
loadTestFile(require.resolve('./event_filters'));
5757
loadTestFile(require.resolve('./host_isolation_exceptions'));
5858
loadTestFile(require.resolve('./blocklists'));
59+
loadTestFile(require.resolve('./endpoint_exceptions'));
5960
});
6061
}

x-pack/solutions/security/test/security_solution_endpoint/services/endpoint_artifacts.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,22 @@ export function EndpointArtifactsTestResourcesProvider({ getService }: FtrProvid
6767
};
6868
}
6969

70+
/**
71+
* Deletes an artifact list along with all of its items (if any).
72+
* @param listId
73+
* @param supertest
74+
*/
75+
async deleteList(
76+
listId: (typeof ENDPOINT_ARTIFACT_LIST_IDS)[number],
77+
supertest: TestAgent = this.supertest
78+
): Promise<void> {
79+
await supertest
80+
.delete(`${EXCEPTION_LIST_URL}?list_id=${listId}&namespace_type=agnostic`)
81+
.set('kbn-xsrf', 'true')
82+
.send()
83+
.then(this.getHttpResponseFailureHandler([404]));
84+
}
85+
7086
async ensureListExists(
7187
listDefinition: CreateExceptionListSchema,
7288
{ supertest = this.supertest, spaceId = DEFAULT_SPACE_ID }: ArtifactCreateOptions = {}

0 commit comments

Comments
 (0)