diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 37d2770b1a9f8..6b7fc8201578c 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -15419,9 +15419,7 @@ paths: content: application/json: schema: - items: - $ref: '#/components/schemas/Security_Endpoint_Exceptions_API_ExceptionListItem' - type: array + $ref: '#/components/schemas/Security_Endpoint_Exceptions_API_ExceptionListItem' description: Successful response '400': content: diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 3b32e565b0f8e..786cc43013cd1 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -17892,9 +17892,7 @@ paths: content: application/json: schema: - items: - $ref: '#/components/schemas/Security_Endpoint_Exceptions_API_ExceptionListItem' - type: array + $ref: '#/components/schemas/Security_Endpoint_Exceptions_API_ExceptionListItem' description: Successful response '400': content: diff --git a/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/read_endpoint_list_item/read_endpoint_list_item.gen.ts b/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/read_endpoint_list_item/read_endpoint_list_item.gen.ts index e2dc38450bbbd..e9e39003fd77b 100644 --- a/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/read_endpoint_list_item/read_endpoint_list_item.gen.ts +++ b/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/read_endpoint_list_item/read_endpoint_list_item.gen.ts @@ -38,4 +38,4 @@ export type ReadEndpointListItemRequestQueryInput = z.input< >; export type ReadEndpointListItemResponse = z.infer; -export const ReadEndpointListItemResponse = z.array(EndpointListItem); +export const ReadEndpointListItemResponse = EndpointListItem; diff --git a/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/read_endpoint_list_item/read_endpoint_list_item.schema.yaml b/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/read_endpoint_list_item/read_endpoint_list_item.schema.yaml index 31a9ebc7b452c..a5a096e5ed89a 100644 --- a/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/read_endpoint_list_item/read_endpoint_list_item.schema.yaml +++ b/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/api/read_endpoint_list_item/read_endpoint_list_item.schema.yaml @@ -29,9 +29,7 @@ paths: content: application/json: schema: - type: array - items: - $ref: '../model/endpoint_list_common.schema.yaml#/components/schemas/EndpointListItem' + $ref: '../model/endpoint_list_common.schema.yaml#/components/schemas/EndpointListItem' 400: description: Invalid input data content: diff --git a/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/ess/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml b/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/ess/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml index 1f15cef3d0e52..652ffbd2001d5 100644 --- a/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/ess/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/ess/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml @@ -139,9 +139,7 @@ paths: content: application/json: schema: - items: - $ref: '#/components/schemas/EndpointListItem' - type: array + $ref: '#/components/schemas/EndpointListItem' description: Successful response '400': content: diff --git a/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/serverless/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml b/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/serverless/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml index f600c8ca248a3..04bfbddf04c82 100644 --- a/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/serverless/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/solutions/security/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/serverless/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml @@ -139,9 +139,7 @@ paths: content: application/json: schema: - items: - $ref: '#/components/schemas/EndpointListItem' - type: array + $ref: '#/components/schemas/EndpointListItem' description: Successful response '400': content: diff --git a/x-pack/solutions/security/plugins/lists/server/routes/create_endpoint_list_item_route.ts b/x-pack/solutions/security/plugins/lists/server/routes/create_endpoint_list_item_route.ts index d4fb35dfec058..0be215137d373 100644 --- a/x-pack/solutions/security/plugins/lists/server/routes/create_endpoint_list_item_route.ts +++ b/x-pack/solutions/security/plugins/lists/server/routes/create_endpoint_list_item_route.ts @@ -18,7 +18,8 @@ import { LISTS_API_ALL } from '@kbn/security-solution-features/constants'; import type { ListsPluginRouter } from '../types'; import { buildSiemResponse, getExceptionListClient } from './utils'; -import { validateExceptionListSize } from './validate'; +import { endpointDisallowedFields } from './endpoint_disallowed_fields'; +import { validateEndpointExceptionItemEntries, validateExceptionListSize } from './validate'; export const createEndpointListItemRoute = (router: ListsPluginRouter): void => { router.versioned @@ -64,37 +65,54 @@ export const createEndpointListItemRoute = (router: ListsPluginRouter): void => body: `exception list item id: "${itemId}" already exists`, statusCode: 409, }); - } else { - const createdList = await exceptionLists.createEndpointListItem({ - comments, - description, - entries, - itemId, - meta, - name, - osTypes, - tags, - type, - }); + } + + const error = validateEndpointExceptionItemEntries(entries); + if (error != null) { + return siemResponse.error(error); + } + for (const entry of entries) { + if (endpointDisallowedFields.includes(entry.field)) { + return siemResponse.error({ + body: `cannot add endpoint exception item on field ${entry.field}`, + statusCode: 400, + }); + } + } - const { success, data, error } = CreateEndpointListItemResponse.safeParse(createdList); - if (success === false) { - return siemResponse.error({ body: stringifyZodError(error), statusCode: 500 }); - } else { - const listSizeError = await validateExceptionListSize( - exceptionLists, - ENDPOINT_LIST_ID, - 'agnostic' - ); - if (listSizeError != null) { - await exceptionLists.deleteExceptionListItemById({ - id: createdList.id, - namespaceType: 'agnostic', - }); - return siemResponse.error(listSizeError); - } - return response.ok({ body: data ?? {} }); + const createdList = await exceptionLists.createEndpointListItem({ + comments, + description, + entries, + itemId, + meta, + name, + osTypes, + tags, + type, + }); + + const { + success, + data, + error: parseError, + } = CreateEndpointListItemResponse.safeParse(createdList); + if (success === false) { + return siemResponse.error({ body: stringifyZodError(parseError), statusCode: 500 }); + } else { + const listSizeError = await validateExceptionListSize( + exceptionLists, + ENDPOINT_LIST_ID, + 'agnostic' + ); + if (listSizeError != null) { + await exceptionLists.deleteExceptionListItemById({ + id: createdList.id, + namespaceType: 'agnostic', + }); + return siemResponse.error(listSizeError); } + return response.ok({ body: data ?? {} }); } } catch (err) { const error = transformError(err); diff --git a/x-pack/solutions/security/plugins/lists/server/services/exception_lists/exception_list_client.test.ts b/x-pack/solutions/security/plugins/lists/server/services/exception_lists/exception_list_client.test.ts index 3c743167dbe00..32f06b3007ee7 100644 --- a/x-pack/solutions/security/plugins/lists/server/services/exception_lists/exception_list_client.test.ts +++ b/x-pack/solutions/security/plugins/lists/server/services/exception_lists/exception_list_client.test.ts @@ -95,6 +95,50 @@ describe('exception_list_client', () => { return extensionPointStorageContext.exceptionPreUpdate.callback; }, ], + + [ + 'createEndpointListItem', + (): ReturnType => { + const mockOptions = getCreateExceptionListItemOptionsMock(); + return exceptionListClient.createEndpointListItem({ + comments: mockOptions.comments, + description: mockOptions.description, + entries: mockOptions.entries, + itemId: mockOptions.itemId, + meta: mockOptions.meta, + name: mockOptions.name, + osTypes: mockOptions.osTypes, + tags: mockOptions.tags, + type: mockOptions.type, + }); + }, + (): ExtensionPointStorageContextMock['exceptionPreCreate']['callback'] => { + return extensionPointStorageContext.exceptionPreCreate.callback; + }, + ], + + [ + 'updateEndpointListItem', + (): ReturnType => { + const mockOptions = getUpdateExceptionListItemOptionsMock(); + return exceptionListClient.updateEndpointListItem({ + _version: mockOptions._version, + comments: mockOptions.comments, + description: mockOptions.description, + entries: mockOptions.entries, + id: mockOptions.id, + itemId: mockOptions.itemId, + meta: mockOptions.meta, + name: mockOptions.name, + osTypes: mockOptions.osTypes, + tags: mockOptions.tags, + type: mockOptions.type, + }); + }, + (): ExtensionPointStorageContextMock['exceptionPreUpdate']['callback'] => { + return extensionPointStorageContext.exceptionPreUpdate.callback; + }, + ], ])( 'and calling `ExceptionListClient#%s()`', (methodName, callExceptionListClientMethod, getExtensionPointCallback) => { @@ -294,6 +338,48 @@ describe('exception_list_client', () => { return extensionPointStorageContext.exceptionPreDelete.callback; }, ], + [ + 'getEndpointListItem', + (): ReturnType => { + return exceptionListClient.getEndpointListItem({ + id: '1', + itemId: '1', + }); + }, + (): ExtensionPointStorageContextMock['exceptionPreGetOne']['callback'] => { + return extensionPointStorageContext.exceptionPreGetOne.callback; + }, + ], + [ + 'deleteEndpointListItem', + (): ReturnType => { + return exceptionListClient.deleteEndpointListItem({ + id: '1', + itemId: '1', + }); + }, + (): ExtensionPointStorageContextMock['exceptionPreDelete']['callback'] => { + return extensionPointStorageContext.exceptionPreDelete.callback; + }, + ], + [ + 'findEndpointListItem', + (): ReturnType => { + return exceptionListClient.findEndpointListItem({ + filter: undefined, + page: 1, + perPage: 1, + pit: undefined, + search: undefined, + searchAfter: undefined, + sortField: 'name', + sortOrder: 'asc', + }); + }, + (): ExtensionPointStorageContextMock['exceptionPreSingleListFind']['callback'] => { + return extensionPointStorageContext.exceptionPreSingleListFind.callback; + }, + ], [ 'importExceptionListAndItems', (): ReturnType => { diff --git a/x-pack/solutions/security/plugins/lists/server/services/exception_lists/exception_list_client.ts b/x-pack/solutions/security/plugins/lists/server/services/exception_lists/exception_list_client.ts index c6c8c3f30ae05..2860e17d1f9e8 100644 --- a/x-pack/solutions/security/plugins/lists/server/services/exception_lists/exception_list_client.ts +++ b/x-pack/solutions/security/plugins/lists/server/services/exception_lists/exception_list_client.ts @@ -290,7 +290,8 @@ export class ExceptionListClient { }: CreateEndpointListItemOptions): Promise => { const { savedObjectsClient, user } = this; await this.createEndpointList(); - return createExceptionListItem({ + + let itemData: CreateExceptionListItemOptions = { comments, description, entries, @@ -301,9 +302,27 @@ export class ExceptionListClient { name, namespaceType: 'agnostic', osTypes, - savedObjectsClient, tags, type, + }; + + if (this.enableServerExtensionPoints) { + itemData = await this.serverExtensionsClient.pipeRun( + 'exceptionsListPreCreateItem', + itemData, + this.getServerExtensionCallbackContext(), + (data) => { + return validateData( + createExceptionListItemSchema, + transformCreateExceptionListItemOptionsToCreateExceptionListItemSchema(data) + ); + } + ); + } + + return createExceptionListItem({ + ...itemData, + savedObjectsClient, user, }); }; @@ -364,7 +383,8 @@ export class ExceptionListClient { }: UpdateEndpointListItemOptions): Promise => { const { savedObjectsClient, user } = this; await this.createEndpointList(); - return updateExceptionListItem({ + + let updatedItem: UpdateExceptionListItemOptions = { _version, comments, description, @@ -376,9 +396,27 @@ export class ExceptionListClient { name, namespaceType: 'agnostic', osTypes, - savedObjectsClient, tags, type, + }; + + if (this.enableServerExtensionPoints) { + updatedItem = await this.serverExtensionsClient.pipeRun( + 'exceptionsListPreUpdateItem', + updatedItem, + this.getServerExtensionCallbackContext(), + (data) => { + return validateData( + updateExceptionListItemSchema, + transformUpdateExceptionListItemOptionsToUpdateExceptionListItemSchema(data) + ); + } + ); + } + + return updateExceptionListItem({ + ...updatedItem, + savedObjectsClient, user, }); }; @@ -395,6 +433,15 @@ export class ExceptionListClient { id, }: GetEndpointListItemOptions): Promise => { const { savedObjectsClient } = this; + + if (this.enableServerExtensionPoints) { + await this.serverExtensionsClient.pipeRun( + 'exceptionsListPreGetOneItem', + { id, itemId, namespaceType: 'agnostic' }, + this.getServerExtensionCallbackContext() + ); + } + return getExceptionListItem({ id, itemId, namespaceType: 'agnostic', savedObjectsClient }); }; @@ -793,6 +840,15 @@ export class ExceptionListClient { itemId, }: DeleteEndpointListItemOptions): Promise => { const { savedObjectsClient } = this; + + if (this.enableServerExtensionPoints) { + await this.serverExtensionsClient.pipeRun( + 'exceptionsListPreDeleteItem', + { id, itemId, namespaceType: 'agnostic' }, + this.getServerExtensionCallbackContext() + ); + } + return deleteExceptionListItem({ id, itemId, @@ -829,36 +885,30 @@ export class ExceptionListClient { }: FindExceptionListItemOptions): Promise => { const { savedObjectsClient } = this; + const findOptions = { + filter, + listId, + namespaceType, + page, + perPage, + pit, + searchAfter, + sortField, + sortOrder, + }; + if (this.enableServerExtensionPoints) { await this.serverExtensionsClient.pipeRun( 'exceptionsListPreSingleListFind', - { - filter, - listId, - namespaceType, - page, - perPage, - pit, - searchAfter, - sortField, - sortOrder, - }, + findOptions, this.getServerExtensionCallbackContext() ); } return findExceptionListItem({ - filter, - listId, - namespaceType, - page, - perPage, - pit, + ...findOptions, savedObjectsClient, search, - searchAfter, - sortField, - sortOrder, }); }; @@ -1026,18 +1076,31 @@ export class ExceptionListClient { }: FindEndpointListItemOptions): Promise => { const { savedObjectsClient } = this; await this.createEndpointList(); - return findExceptionListItem({ + + const findOptions = { filter, listId: ENDPOINT_LIST_ID, - namespaceType: 'agnostic', + namespaceType: 'agnostic' as const, page, perPage, pit, - savedObjectsClient, - search, searchAfter, sortField, sortOrder, + }; + + if (this.enableServerExtensionPoints) { + await this.serverExtensionsClient.pipeRun( + 'exceptionsListPreSingleListFind', + findOptions, + this.getServerExtensionCallbackContext() + ); + } + + return findExceptionListItem({ + ...findOptions, + savedObjectsClient, + search, }); }; diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/endpoint_exceptions.ff_enabled.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/endpoint_exceptions.ff_enabled.ts index 5541cb353f668..8929c5e665b87 100644 --- a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/endpoint_exceptions.ff_enabled.ts +++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/endpoint_exceptions.ff_enabled.ts @@ -58,5 +58,6 @@ export default function endpointAPIIntegrationTests(providerContext: FtrProvider }); loadTestFile(require.resolve('./endpoint_exceptions')); + loadTestFile(require.resolve('./endpoint_list_api_rbac')); }); } diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/endpoint_list_api_rbac.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/endpoint_list_api_rbac.ts new file mode 100644 index 0000000000000..6048cca9c3e45 --- /dev/null +++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/endpoint_list_api_rbac.ts @@ -0,0 +1,363 @@ +/* + * 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 TestAgent from 'supertest/lib/agent'; +import expect from '@kbn/expect'; +import { ENDPOINT_LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; +import { GLOBAL_ARTIFACT_TAG } from '@kbn/security-solution-plugin/common/endpoint/service/artifacts/constants'; +import { ExceptionsListItemGenerator } from '@kbn/security-solution-plugin/common/endpoint/data_generators/exceptions_list_item_generator'; +import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { SECURITY_FEATURE_ID } from '@kbn/security-solution-plugin/common'; +import { + buildPerPolicyTag, + isPolicySelectionTag, +} from '@kbn/security-solution-plugin/common/endpoint/service/artifacts/utils'; +import type { ArtifactTestData } from '@kbn/test-suites-xpack-security-endpoint/services/endpoint_artifacts'; +import type { PolicyTestResourceInfo } from '@kbn/test-suites-xpack-security-endpoint/services/endpoint_policy'; +import { ROLE } from '../../../../config/services/security_solution_edr_workflows_roles_users'; +import type { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows'; + +export default function ({ getService }: FtrProviderContext) { + const rolesUsersProvider = getService('rolesUsersProvider'); + const endpointPolicyTestResources = getService('endpointPolicyTestResources'); + const endpointArtifactTestResources = getService('endpointArtifactTestResources'); + const utils = getService('securitySolutionUtils'); + const config = getService('config'); + + const IS_ENDPOINT_EXCEPTION_MOVE_FF_ENABLED = ( + config.get('kbnTestServer.serverArgs', []) as string[] + ) + .find((s) => s.startsWith('--xpack.securitySolution.enableExperimental')) + ?.includes('endpointExceptionsMovedUnderManagement'); + + // @skipInServerlessMKI due to authentication issues - we should migrate from Basic to Bearer token when available + // @skipInServerlessMKI - if you are removing this annotation, make sure to add the test suite to the MKI pipeline in .buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_defend_workflows.yml + describe('@ess @serverless @skipInServerlessMKI Endpoint List API (deprecated): RBAC and Validation', function () { + let fleetEndpointPolicy: PolicyTestResourceInfo; + + let t1AnalystSupertest: TestAgent; + let endpointPolicyManagerSupertest: TestAgent; + + before(async () => { + t1AnalystSupertest = await utils.createSuperTest(ROLE.t1_analyst); + endpointPolicyManagerSupertest = await utils.createSuperTest(ROLE.endpoint_policy_manager); + + // Create an endpoint policy in fleet we can work with + fleetEndpointPolicy = await endpointPolicyTestResources.createPolicy(); + }); + + after(async () => { + if (fleetEndpointPolicy) { + await fleetEndpointPolicy.cleanup(); + } + }); + + const anEndpointArtifactError = (res: { body: { message: string } }) => { + expect(res.body.message).to.match(/EndpointArtifactError/); + }; + const anErrorMessageWith = ( + value: string | RegExp + ): ((res: { body: { message: string } }) => void) => { + return (res) => { + if (value instanceof RegExp) { + expect(res.body.message).to.match(value); + } else { + expect(res.body.message).to.be(value); + } + }; + }; + + const exceptionsGenerator = new ExceptionsListItemGenerator(); + let endpointExceptionData: ArtifactTestData; + + type EndpointListApiCallsInterface = Array<{ + method: keyof Pick; + info?: string; + path: string; + // The body just needs to have the properties we care about in the tests. This should cover most + // mocks used for testing that support different interfaces + getBody: () => BodyReturnType; + }>; + + const endpointListCalls: EndpointListApiCallsInterface< + Pick & { + id?: string; + _version?: string; + } + > = [ + { + method: 'post', + info: 'create single item', + path: ENDPOINT_LIST_ITEM_URL, + getBody: () => + exceptionsGenerator.generateEndpointExceptionForCreate({ + tags: endpointExceptionData?.artifact.tags || [ + buildPerPolicyTag(fleetEndpointPolicy.packagePolicy.id), + ], + }), + }, + { + method: 'put', + info: 'update single item', + path: ENDPOINT_LIST_ITEM_URL, + getBody: () => + exceptionsGenerator.generateEndpointExceptionForUpdate({ + id: endpointExceptionData.artifact.id, + item_id: endpointExceptionData.artifact.item_id, + tags: endpointExceptionData.artifact.tags, + _version: endpointExceptionData.artifact._version, + }), + }, + ]; + + const needsWritePrivilege: EndpointListApiCallsInterface = [ + { + method: 'delete', + info: 'delete single item', + get path() { + return `${ENDPOINT_LIST_ITEM_URL}?item_id=${endpointExceptionData.artifact.item_id}`; + }, + getBody: () => undefined, + }, + ]; + + const needsReadPrivilege: EndpointListApiCallsInterface = [ + { + method: 'get', + info: 'single item', + get path() { + return `${ENDPOINT_LIST_ITEM_URL}?item_id=${endpointExceptionData.artifact.item_id}`; + }, + getBody: () => undefined, + }, + { + method: 'get', + info: 'find items', + get path() { + return `${ENDPOINT_LIST_ITEM_URL}/_find?page=1&per_page=1&sort_field=name&sort_order=asc`; + }, + getBody: () => undefined, + }, + ]; + + beforeEach(async () => { + endpointExceptionData = await endpointArtifactTestResources.createEndpointException({ + tags: [buildPerPolicyTag(fleetEndpointPolicy.packagePolicy.id)], + }); + }); + + afterEach(async () => { + if (endpointExceptionData) { + await endpointExceptionData.cleanup(); + } + }); + + describe('and has authorization to manage endpoint security', () => { + for (const endpointListApiCall of endpointListCalls) { + it(`should work on [${endpointListApiCall.method}] with valid entry`, async () => { + const body = endpointListApiCall.getBody(); + + // Using superuser here as we need custom license for this action + await endpointPolicyManagerSupertest[endpointListApiCall.method](endpointListApiCall.path) + .set('kbn-xsrf', 'true') + .send(body) + .expect(200); + + const deleteUrl = `${ENDPOINT_LIST_ITEM_URL}?item_id=${body.item_id}`; + await endpointPolicyManagerSupertest.delete(deleteUrl).set('kbn-xsrf', 'true'); + }); + + it(`should work on [${endpointListApiCall.method}] if more than one OS is set`, async () => { + const body = endpointListApiCall.getBody(); + body.os_types = ['linux', 'windows']; + + await endpointPolicyManagerSupertest[endpointListApiCall.method](endpointListApiCall.path) + .set('kbn-xsrf', 'true') + .send(body) + .expect(200); + + const deleteUrl = `${ENDPOINT_LIST_ITEM_URL}?item_id=${body.item_id}`; + await endpointPolicyManagerSupertest.delete(deleteUrl).set('kbn-xsrf', 'true'); + }); + + if (IS_ENDPOINT_EXCEPTION_MOVE_FF_ENABLED) { + it(`should accept item on [${endpointListApiCall.method}] if no assignment tag is present`, async () => { + const requestBody = endpointListApiCall.getBody(); + requestBody.tags = []; + + await endpointPolicyManagerSupertest[endpointListApiCall.method]( + endpointListApiCall.path + ) + .set('kbn-xsrf', 'true') + .send(requestBody) + .expect(200) + .expect(({ body }) => expect(body.tags).to.not.contain(GLOBAL_ARTIFACT_TAG)); + + const deleteUrl = `${ENDPOINT_LIST_ITEM_URL}?item_id=${requestBody.item_id}`; + await endpointPolicyManagerSupertest.delete(deleteUrl).set('kbn-xsrf', 'true'); + }); + } else { + it(`should add global artifact tag on [${endpointListApiCall.method}] if no assignment tag is present`, async () => { + const requestBody = endpointListApiCall.getBody(); + requestBody.tags = []; + + await endpointPolicyManagerSupertest[endpointListApiCall.method]( + endpointListApiCall.path + ) + .set('kbn-xsrf', 'true') + .send(requestBody) + .expect(200) + .expect(({ body }) => expect(body.tags).to.contain(GLOBAL_ARTIFACT_TAG)); + + const deleteUrl = `${ENDPOINT_LIST_ITEM_URL}?item_id=${requestBody.item_id}`; + await endpointPolicyManagerSupertest.delete(deleteUrl).set('kbn-xsrf', 'true'); + }); + } + + if (IS_ENDPOINT_EXCEPTION_MOVE_FF_ENABLED) { + it(`should error on [${endpointListApiCall.method}] if policy id is invalid`, async () => { + const body = endpointListApiCall.getBody(); + body.tags = [buildPerPolicyTag('123')]; + + await endpointPolicyManagerSupertest[endpointListApiCall.method]( + endpointListApiCall.path + ) + .set('kbn-xsrf', 'true') + .send(body) + .expect(400) + .expect(anEndpointArtifactError) + .expect(anErrorMessageWith(/invalid policy ids/)); + }); + } + } + for (const endpointListApiCall of [...needsWritePrivilege, ...needsReadPrivilege]) { + it(`should not error on [${endpointListApiCall.method}] - [${endpointListApiCall.info}]`, async () => { + await endpointPolicyManagerSupertest[endpointListApiCall.method](endpointListApiCall.path) + .set('kbn-xsrf', 'true') + .send(endpointListApiCall.getBody() as object) + .expect(200); + }); + } + }); + + describe('@skipInServerless and user has endpoint exception access but no global artifact access', () => { + let noGlobalArtifactSupertest: TestAgent; + + before(async () => { + const loadedRole = await rolesUsersProvider.loader.create({ + name: 'no_global_artifact_role_endpoint_list', + kibana: [ + { + base: [], + feature: { + [SECURITY_FEATURE_ID]: ['read', 'endpoint_exceptions_all'], + }, + spaces: ['*'], + }, + ], + elasticsearch: { cluster: [], indices: [], run_as: [] }, + }); + + noGlobalArtifactSupertest = await utils.createSuperTest(loadedRole.username); + }); + + after(async () => { + await rolesUsersProvider.loader.delete('no_global_artifact_role_endpoint_list'); + }); + + for (const endpointListApiCall of endpointListCalls) { + if (IS_ENDPOINT_EXCEPTION_MOVE_FF_ENABLED) { + it(`should error on [${endpointListApiCall.method}] - [${endpointListApiCall.info}] when global artifact is the target`, async () => { + const requestBody = endpointListApiCall.getBody(); + // keep space tag, but replace any per-policy tags with a global tag + requestBody.tags = [ + ...requestBody.tags.filter((tag) => !isPolicySelectionTag(tag)), + GLOBAL_ARTIFACT_TAG, + ]; + + await noGlobalArtifactSupertest[endpointListApiCall.method](endpointListApiCall.path) + .set('kbn-xsrf', 'true') + .send(requestBody as object) + .expect(403) + .expect(anEndpointArtifactError) + .expect( + anErrorMessageWith( + /Endpoint authorization failure. Management of global artifacts requires additional privilege \(global artifact management\)/ + ) + ); + }); + + it(`should work on [${endpointListApiCall.method}] - [${endpointListApiCall.info}] when per-policy artifact is the target`, async () => { + const requestBody = endpointListApiCall.getBody(); + + // remove existing tag + requestBody.tags = requestBody.tags.filter((tag) => !isPolicySelectionTag(tag)); + + await noGlobalArtifactSupertest[endpointListApiCall.method](endpointListApiCall.path) + .set('kbn-xsrf', 'true') + .send(requestBody as object) + .expect(200); + }); + } else { + it(`should error on [${endpointListApiCall.method}] - [${endpointListApiCall.info}]`, async () => { + await noGlobalArtifactSupertest[endpointListApiCall.method](endpointListApiCall.path) + .set('kbn-xsrf', 'true') + .send(endpointListApiCall.getBody() as object) + .expect(403) + .expect(anEndpointArtifactError) + .expect( + anErrorMessageWith( + /Endpoint authorization failure. Management of global artifacts requires additional privilege \(global artifact management\)/ + ) + ); + }); + } + } + }); + + describe('@skipInServerless and user has authorization to read endpoint exceptions', function () { + let hunterSupertest: TestAgent; + + before(async () => { + hunterSupertest = await utils.createSuperTest(ROLE.hunter); + }); + + for (const endpointListApiCall of [...endpointListCalls, ...needsWritePrivilege]) { + it(`should error on [${endpointListApiCall.method}] - [${endpointListApiCall.info}]`, async () => { + await hunterSupertest[endpointListApiCall.method](endpointListApiCall.path) + .set('kbn-xsrf', 'true') + .send(endpointListApiCall.getBody() as object) + .expect(403); + }); + } + + for (const endpointListApiCall of needsReadPrivilege) { + it(`should not error on [${endpointListApiCall.method}] - [${endpointListApiCall.info}]`, async () => { + await hunterSupertest[endpointListApiCall.method](endpointListApiCall.path) + .set('kbn-xsrf', 'true') + .send(endpointListApiCall.getBody() as object) + .expect(200); + }); + } + }); + + describe('and user has no authorization to endpoint exceptions', () => { + for (const endpointListApiCall of [ + ...endpointListCalls, + ...needsWritePrivilege, + ...needsReadPrivilege, + ]) { + it(`should error on [${endpointListApiCall.method}] - [${endpointListApiCall.info}]`, async () => { + await t1AnalystSupertest[endpointListApiCall.method](endpointListApiCall.path) + .set('kbn-xsrf', 'true') + .send(endpointListApiCall.getBody() as object) + .expect(403); + }); + } + }); + }); +} diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/index.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/index.ts index 460987a5991d2..e2f5ac4256f4a 100644 --- a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/index.ts +++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/index.ts @@ -58,5 +58,6 @@ export default function endpointAPIIntegrationTests(providerContext: FtrProvider loadTestFile(require.resolve('./host_isolation_exceptions')); loadTestFile(require.resolve('./blocklists')); loadTestFile(require.resolve('./endpoint_exceptions')); + loadTestFile(require.resolve('./endpoint_list_api_rbac')); }); }