diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/artifacts/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/artifacts/constants.ts index 9917742d7b7cc..f657577d40ffe 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/artifacts/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/artifacts/constants.ts @@ -15,6 +15,9 @@ export const GLOBAL_ARTIFACT_TAG = `${BY_POLICY_ARTIFACT_TAG_PREFIX}all`; export const FILTER_PROCESS_DESCENDANTS_TAG = 'filter_process_descendants'; +/** The tag prefix that tracks the space(s) that is considered the "owner" of the artifact. */ +export const OWNER_SPACE_ID_TAG_PREFIX = 'ownerSpaceId:'; + export const PROCESS_DESCENDANT_EVENT_FILTER_EXTRA_ENTRY: EntryMatch = Object.freeze({ field: 'event.category', operator: 'included', diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/artifacts/utils.test.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/artifacts/utils.test.ts index dba34da5bf1e5..fa359d6f0a64f 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/artifacts/utils.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/artifacts/utils.test.ts @@ -13,15 +13,19 @@ import { GLOBAL_ARTIFACT_TAG, } from './constants'; import { + buildSpaceOwnerIdTag, createExceptionListItemForCreate, + getArtifactOwnerSpaceIds, getArtifactTagsByPolicySelection, getEffectedPolicySelectionByTags, getPolicyIdsFromArtifact, + hasArtifactOwnerSpaceId, isArtifactByPolicy, isArtifactGlobal, isFilterProcessDescendantsEnabled, isFilterProcessDescendantsTag, isPolicySelectionTag, + setArtifactOwnerSpaceId, } from './utils'; describe('Endpoint artifact utilities', () => { @@ -193,4 +197,47 @@ describe('Endpoint artifact utilities', () => { }); }); }); + + describe('when using `buildSpaceOwnerIdTag()`', () => { + it('should return an artifact tag', () => { + expect(buildSpaceOwnerIdTag('abc')).toEqual(`ownerSpaceId:abc`); + }); + }); + + describe('when using `getArtifactOwnerSpaceIds()`', () => { + it.each` + name | tags | expectedResult + ${'expected array of values'} | ${{ tags: [buildSpaceOwnerIdTag('abc'), buildSpaceOwnerIdTag('123')] }} | ${['abc', '123']} + ${'empty array if no tags'} | ${{}} | ${[]} + ${'empty array if no ownerSpaceId tags'} | ${{ tags: ['one', 'two'] }} | ${[]} + `('should return $name', ({ tags, expectedResult }) => { + expect(getArtifactOwnerSpaceIds(tags)).toEqual(expectedResult); + }); + }); + + describe('when using `hasArtifactOwnerSpaceId()`', () => { + it.each` + name | tags | expectedResult + ${'artifact has tag with space id'} | ${{ tags: [buildSpaceOwnerIdTag('abc')] }} | ${true} + ${'artifact does not have tag with space id'} | ${{ tags: ['123'] }} | ${false} + `('should return $expectedResult when $name', ({ tags, expectedResult }) => { + expect(hasArtifactOwnerSpaceId(tags)).toEqual(expectedResult); + }); + }); + + describe('when using `setArtifactOwnerSpaceId()`', () => { + it('should set owner space ID if item does not currently have one matching the space id', () => { + const item = { tags: [buildSpaceOwnerIdTag('foo')] }; + setArtifactOwnerSpaceId(item, 'abc'); + + expect(item).toEqual({ tags: [buildSpaceOwnerIdTag('foo'), buildSpaceOwnerIdTag('abc')] }); + }); + + it('should not add another owner space ID if item already has one that matches the space id', () => { + const item = { tags: [buildSpaceOwnerIdTag('abc')] }; + setArtifactOwnerSpaceId(item, 'abc'); + + expect(item).toEqual({ tags: [buildSpaceOwnerIdTag('abc')] }); + }); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/artifacts/utils.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/artifacts/utils.ts index 322cd87fd7bea..e9026f092c7eb 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/artifacts/utils.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/artifacts/utils.ts @@ -16,6 +16,7 @@ import { BY_POLICY_ARTIFACT_TAG_PREFIX, FILTER_PROCESS_DESCENDANTS_TAG, GLOBAL_ARTIFACT_TAG, + OWNER_SPACE_ID_TAG_PREFIX, } from './constants'; export type TagFilter = (tag: string) => boolean; @@ -118,3 +119,62 @@ export const createExceptionListItemForCreate = (listId: string): CreateExceptio os_types: ['windows'], }; }; + +/** + * Returns an array with all owner space IDs for the artifact + */ +export const getArtifactOwnerSpaceIds = ( + item: Partial> +): string[] => { + return (item.tags ?? []).reduce((acc, tag) => { + if (tag.startsWith(OWNER_SPACE_ID_TAG_PREFIX)) { + acc.push(tag.substring(OWNER_SPACE_ID_TAG_PREFIX.length)); + } + + return acc; + }, [] as string[]); +}; + +/** Returns an Artifact `tag` value for a given space id */ +export const buildSpaceOwnerIdTag = (spaceId: string): string => { + if (spaceId.trim() === '') { + throw new Error('spaceId must be a string with a length greater than zero.'); + } + + return `${OWNER_SPACE_ID_TAG_PREFIX}${spaceId}`; +}; + +/** + * Sets the owner space id on the given artifact, if not already present. + * + * NOTE: this utility will mutate the artifact exception list item provided on input. + * + * @param item + * @param spaceId + */ +export const setArtifactOwnerSpaceId = ( + item: Partial>, + spaceId: string +): void => { + if (spaceId.trim() === '') { + throw new Error('spaceId must be a string with a length greater than zero.'); + } + + if (!getArtifactOwnerSpaceIds(item).includes(spaceId)) { + if (!item.tags) { + item.tags = []; + } + + item.tags.push(buildSpaceOwnerIdTag(spaceId)); + } +}; + +/** + * Checks to see if the artifact item has at least 1 owner space id tag + * @param item + */ +export const hasArtifactOwnerSpaceId = ( + item: Partial> +): boolean => { + return (item.tags ?? []).some((tag) => tag.startsWith(OWNER_SPACE_ID_TAG_PREFIX)); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index 92e3de8a10645..ddebefd0bddc0 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -25,7 +25,9 @@ import type { AlertingServerStart } from '@kbn/alerting-plugin/server'; import type { CloudSetup } from '@kbn/cloud-plugin/server'; import type { FleetActionsClientInterface } from '@kbn/fleet-plugin/server/services/actions/types'; import type { PluginStartContract as ActionsPluginStartContract } from '@kbn/actions-plugin/server'; +import type { Space } from '@kbn/spaces-plugin/common'; import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; +import type { SpacesServiceStart } from '@kbn/spaces-plugin/server'; import type { TelemetryConfigProvider } from '../../common/telemetry_config/telemetry_config_provider'; import { SavedObjectsClientFactory } from './services/saved_objects'; import type { ResponseActionsClient } from './services'; @@ -88,6 +90,7 @@ export interface EndpointAppContextServiceStartContract { savedObjectsServiceStart: SavedObjectsServiceStart; connectorActions: ActionsPluginStartContract; telemetryConfigProvider: TelemetryConfigProvider; + spacesService: SpacesServiceStart | undefined; } /** @@ -430,4 +433,12 @@ export class EndpointAppContextService { } return this.setupDependencies.telemetry; } + + public getActiveSpace(httpRequest: KibanaRequest): Promise { + if (!this.startDependencies?.spacesService) { + throw new EndpointAppContentServicesNotStartedError(); + } + + return this.startDependencies.spacesService.getActiveSpace(httpRequest); + } } diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/mocks/mocks.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/mocks/mocks.ts index ea4be1a2870ff..f74c0509da2b1 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/mocks/mocks.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/mocks/mocks.ts @@ -50,6 +50,8 @@ import { unsecuredActionsClientMock } from '@kbn/actions-plugin/server/unsecured import type { PluginStartContract as ActionPluginStartContract } from '@kbn/actions-plugin/server'; import type { Mutable } from 'utility-types'; import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; +import { spacesMock } from '@kbn/spaces-plugin/server/mocks'; +import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import { createTelemetryConfigProviderMock } from '../../../common/telemetry_config/mocks'; import { createSavedObjectsClientFactoryMock } from '../services/saved_objects/saved_objects_client_factory.mocks'; import { EndpointMetadataService } from '../services/metadata'; @@ -148,6 +150,7 @@ export const createMockEndpointAppContextService = ( savedObjects: createSavedObjectsClientFactoryMock({ savedObjectsServiceStart }).service, isServerless: jest.fn().mockReturnValue(false), getInternalEsClient: jest.fn().mockReturnValue(esClient), + getActiveSpace: jest.fn(async () => DEFAULT_SPACE_ID), } as unknown as jest.Mocked; }; @@ -175,7 +178,7 @@ type CreateMockEndpointAppContextServiceStartContractType = Omit< export const createMockEndpointAppContextServiceStartContract = (): CreateMockEndpointAppContextServiceStartContractType => { const config = createMockConfig(); - + const spacesService = spacesMock.createStart().spacesService; const logger = loggingSystemMock.create().get('mock_endpoint_app_context'); const security = securityServiceMock.createStart() as unknown as DeeplyMockedKeys; @@ -218,6 +221,7 @@ export const createMockEndpointAppContextServiceStartContract = getUnsecuredActionsClient: jest.fn().mockReturnValue(unsecuredActionsClientMock.create()), } as unknown as jest.Mocked, telemetryConfigProvider: createTelemetryConfigProviderMock(), + spacesService, }; return startContract; 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 4ad1d3f699990..dfcb222a3a675 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 @@ -11,6 +11,7 @@ import { isEqual } from 'lodash/fp'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import { OperatingSystem } from '@kbn/securitysolution-utils'; +import { setArtifactOwnerSpaceId } from '../../../../common/endpoint/service/artifacts/utils'; import type { FeatureKeys } from '../../../endpoint/services'; import type { EndpointAuthz } from '../../../../common/endpoint/types/authz'; import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services'; @@ -196,4 +197,25 @@ export class BaseValidator { return false; } + + /** + * Update the artifact item (if necessary) with a `ownerSpaceId` tag using the HTTP request's active space + * @param item + * @protected + */ + protected async setOwnerSpaceId( + item: Partial> + ): Promise { + if (this.endpointAppContext.experimentalFeatures.endpointManagementSpaceAwarenessEnabled) { + if (!this.request) { + throw new EndpointArtifactExceptionValidationError( + 'Unable to determine space id. Missing HTTP Request object', + 500 + ); + } + + const spaceId = (await this.endpointAppContext.getActiveSpace(this.request)).id; + setArtifactOwnerSpaceId(item, spaceId); + } + } } 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 d5c66dc2c8e6e..a7fda50a2b6cc 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 @@ -15,6 +15,7 @@ import type { UpdateExceptionListItemOptions, } from '@kbn/lists-plugin/server'; import { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants'; +import { hasArtifactOwnerSpaceId } from '../../../../common/endpoint/service/artifacts/utils'; import { BaseValidator } from './base_validator'; import type { ExceptionItemLikeOptions } from '../types'; import { isValidHash } from '../../../../common/endpoint/service/artifacts/validations'; @@ -243,6 +244,8 @@ export class BlocklistValidator extends BaseValidator { await this.validateCanCreateByPolicyArtifacts(item); await this.validateByPolicyItem(item); + await this.setOwnerSpaceId(item); + return item; } @@ -297,6 +300,10 @@ export class BlocklistValidator extends BaseValidator { await this.validateByPolicyItem(updatedItem); + if (!hasArtifactOwnerSpaceId(_updatedItem)) { + await this.setOwnerSpaceId(_updatedItem); + } + return _updatedItem; } 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 23d1d28ba0a59..292389253417e 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 @@ -10,6 +10,7 @@ import type { UpdateExceptionListItemOptions, } from '@kbn/lists-plugin/server'; import { ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants'; +import { hasArtifactOwnerSpaceId } from '../../../../common/endpoint/service/artifacts/utils'; import { BaseValidator } from './base_validator'; export class EndpointExceptionsValidator extends BaseValidator { @@ -27,11 +28,19 @@ export class EndpointExceptionsValidator extends BaseValidator { async validatePreCreateItem(item: CreateExceptionListItemOptions) { await this.validateHasWritePrivilege(); + + await this.setOwnerSpaceId(item); + return item; } async validatePreUpdateItem(item: UpdateExceptionListItemOptions) { await this.validateHasWritePrivilege(); + + if (!hasArtifactOwnerSpaceId(item)) { + await this.setOwnerSpaceId(item); + } + return item; } 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 1e68355d20037..8b67ffef51e20 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 @@ -14,6 +14,7 @@ import type { UpdateExceptionListItemOptions, } from '@kbn/lists-plugin/server'; +import { hasArtifactOwnerSpaceId } from '../../../../common/endpoint/service/artifacts/utils'; import type { ExceptionItemLikeOptions } from '../types'; import { BaseValidator } from './base_validator'; @@ -59,6 +60,8 @@ export class EventFilterValidator extends BaseValidator { await this.validateByPolicyItem(item); } + await this.setOwnerSpaceId(item); + return item; } @@ -83,6 +86,11 @@ export class EventFilterValidator extends BaseValidator { } await this.validateByPolicyItem(updatedItem); + + if (!hasArtifactOwnerSpaceId(_updatedItem)) { + await this.setOwnerSpaceId(_updatedItem); + } + return _updatedItem; } 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 debd4b021ba09..7921da93dc20d 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 @@ -12,6 +12,7 @@ import type { CreateExceptionListItemOptions, UpdateExceptionListItemOptions, } from '@kbn/lists-plugin/server'; +import { hasArtifactOwnerSpaceId } from '../../../../common/endpoint/service/artifacts/utils'; import { BaseValidator, BasicEndpointExceptionDataSchema } from './base_validator'; import { EndpointArtifactExceptionValidationError } from './errors'; import type { ExceptionItemLikeOptions } from '../types'; @@ -79,6 +80,8 @@ export class HostIsolationExceptionsValidator extends BaseValidator { await this.validateHostIsolationData(item); await this.validateByPolicyItem(item); + await this.setOwnerSpaceId(item); + return item; } @@ -91,6 +94,10 @@ export class HostIsolationExceptionsValidator extends BaseValidator { await this.validateHostIsolationData(updatedItem); await this.validateByPolicyItem(updatedItem); + if (!hasArtifactOwnerSpaceId(_updatedItem)) { + await this.setOwnerSpaceId(_updatedItem); + } + return _updatedItem; } 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 3b8a77a964006..da0b405a9ccb3 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 @@ -15,6 +15,7 @@ import type { CreateExceptionListItemOptions, UpdateExceptionListItemOptions, } from '@kbn/lists-plugin/server'; +import { hasArtifactOwnerSpaceId } from '../../../../common/endpoint/service/artifacts/utils'; import { BaseValidator } from './base_validator'; import type { ExceptionItemLikeOptions } from '../types'; import type { TrustedAppConditionEntry as ConditionEntry } from '../../../../common/endpoint/types'; @@ -207,6 +208,8 @@ export class TrustedAppValidator extends BaseValidator { await this.validateCanCreateByPolicyArtifacts(item); await this.validateByPolicyItem(item); + await this.setOwnerSpaceId(item); + return item; } @@ -256,6 +259,10 @@ export class TrustedAppValidator extends BaseValidator { await this.validateByPolicyItem(updatedItem); + if (!hasArtifactOwnerSpaceId(_updatedItem)) { + await this.setOwnerSpaceId(_updatedItem); + } + return _updatedItem; } diff --git a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts index dea527b73fd76..93ee94f6b3944 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts @@ -639,6 +639,7 @@ export class Plugin implements ISecuritySolutionPlugin { productFeaturesService, savedObjectsServiceStart: core.savedObjects, connectorActions: plugins.actions, + spacesService: plugins.spaces?.spacesService, }); if (this.lists && plugins.taskManager && plugins.fleet) { diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/spaces/trial_license_complete_tier/space_awareness.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/spaces/trial_license_complete_tier/space_awareness.ts index 9c83451111f95..e9f4a1289d6fe 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/spaces/trial_license_complete_tier/space_awareness.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/spaces/trial_license_complete_tier/space_awareness.ts @@ -15,12 +15,21 @@ import { HOST_METADATA_GET_ROUTE, HOST_METADATA_LIST_ROUTE, } from '@kbn/security-solution-plugin/common/endpoint/constants'; +import { + ENDPOINT_ARTIFACT_LISTS, + EXCEPTION_LIST_ITEM_URL, +} from '@kbn/securitysolution-list-constants'; +import { buildSpaceOwnerIdTag } from '@kbn/security-solution-plugin/common/endpoint/service/artifacts/utils'; +import { exceptionItemToCreateExceptionItem } from '@kbn/security-solution-plugin/common/endpoint/data_generators/exceptions_list_item_generator'; +import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { ArtifactTestData } from '../../../../../security_solution_endpoint/services/endpoint_artifacts'; import { createSupertestErrorLogger } from '../../utils'; import { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows'; export default function ({ getService }: FtrProviderContext) { const utils = getService('securitySolutionUtils'); const endpointTestresources = getService('endpointTestResources'); + const endpointArtifactTestResources = getService('endpointArtifactTestResources'); const kbnServer = getService('kibanaServer'); const log = getService('log'); @@ -186,5 +195,78 @@ export default function ({ getService }: FtrProviderContext) { expect(body.data[dataSpaceB.hosts[0].agent.id].found).to.eql(false); }); }); + + describe(`Artifact management (via Lists plugin)`, () => { + const artifactLists = Object.keys(ENDPOINT_ARTIFACT_LISTS); + + for (const artifactList of artifactLists) { + const listInfo = + ENDPOINT_ARTIFACT_LISTS[artifactList as keyof typeof ENDPOINT_ARTIFACT_LISTS]; + + describe(`for ${listInfo.name}`, () => { + let itemDataSpaceA: ArtifactTestData; + + beforeEach(async () => { + itemDataSpaceA = await endpointArtifactTestResources.createArtifact( + listInfo.id, + { tags: [] }, + { supertest: adminSupertest, spaceId: dataSpaceA.spaceId } + ); + }); + + afterEach(async () => { + if (itemDataSpaceA) { + await itemDataSpaceA.cleanup(); + // @ts-expect-error assigning `undefined` + itemDataSpaceA = undefined; + } + }); + + it('should add owner space id when item is created', async () => { + expect(itemDataSpaceA.artifact.tags).to.include.string( + buildSpaceOwnerIdTag(dataSpaceA.spaceId) + ); + }); + + it('should not add owner space id during artifact update if one is already present', async () => { + const { body } = await adminSupertest + .put(addSpaceIdToPath('/', dataSpaceA.spaceId, EXCEPTION_LIST_ITEM_URL)) + .set('elastic-api-version', '2023-10-31') + .set('x-elastic-internal-origin', 'kibana') + .set('kbn-xsrf', 'true') + .on('error', createSupertestErrorLogger(log)) + .send( + exceptionItemToCreateExceptionItem({ + ...itemDataSpaceA.artifact, + description: 'item was updated', + }) + ) + .expect(200); + + expect((body as ExceptionListItemSchema).tags).to.eql(itemDataSpaceA.artifact.tags); + }); + + it('should add owner space id when item is updated, if one is not present', async () => { + const { body } = await adminSupertest + .put(addSpaceIdToPath('/', dataSpaceA.spaceId, EXCEPTION_LIST_ITEM_URL)) + .set('elastic-api-version', '2023-10-31') + .set('x-elastic-internal-origin', 'kibana') + .set('kbn-xsrf', 'true') + .on('error', createSupertestErrorLogger(log)) + .send( + exceptionItemToCreateExceptionItem({ + ...itemDataSpaceA.artifact, + tags: [], + }) + ) + .expect(200); + + expect((body as ExceptionListItemSchema).tags).to.eql([ + buildSpaceOwnerIdTag(dataSpaceA.spaceId), + ]); + }); + }); + } + }); }); } diff --git a/x-pack/test/security_solution_endpoint/services/endpoint_artifacts.ts b/x-pack/test/security_solution_endpoint/services/endpoint_artifacts.ts index ce0d5ce6cda41..84197cff208a1 100644 --- a/x-pack/test/security_solution_endpoint/services/endpoint_artifacts.ts +++ b/x-pack/test/security_solution_endpoint/services/endpoint_artifacts.ts @@ -24,6 +24,8 @@ import { EVENT_FILTER_LIST_DEFINITION } from '@kbn/security-solution-plugin/publ import { HOST_ISOLATION_EXCEPTIONS_LIST_DEFINITION } from '@kbn/security-solution-plugin/public/management/pages/host_isolation_exceptions/constants'; import { BLOCKLISTS_LIST_DEFINITION } from '@kbn/security-solution-plugin/public/management/pages/blocklist/constants'; import { ManifestConstants } from '@kbn/security-solution-plugin/server/endpoint/lib/artifacts'; +import TestAgent from 'supertest/lib/agent'; +import { addSpaceIdToPath, DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import { FtrService } from '../../functional/ftr_provider_context'; import { InternalUnifiedManifestSchemaResponseType } from '../apps/integrations/mocks'; @@ -32,6 +34,11 @@ export interface ArtifactTestData { cleanup: () => Promise; } +export interface ArtifactCreateOptions { + supertest?: TestAgent; + spaceId?: string; +} + export class EndpointArtifactsTestResources extends FtrService { private readonly exceptionsGenerator = new ExceptionsListItemGenerator(); private readonly supertest = this.ctx.getService('supertest'); @@ -50,20 +57,24 @@ export class EndpointArtifactsTestResources extends FtrService { }; } - private async ensureListExists(listDefinition: CreateExceptionListSchema): Promise { + private async ensureListExists( + listDefinition: CreateExceptionListSchema, + { supertest = this.supertest, spaceId = DEFAULT_SPACE_ID }: ArtifactCreateOptions = {} + ): Promise { // attempt to create it and ignore 409 (already exists) errors - await this.supertest - .post(EXCEPTION_LIST_URL) + await supertest + .post(addSpaceIdToPath('/', spaceId, EXCEPTION_LIST_URL)) .set('kbn-xsrf', 'true') .send(listDefinition) .then(this.getHttpResponseFailureHandler([409])); } private async createExceptionItem( - createPayload: CreateExceptionListItemSchema + createPayload: CreateExceptionListItemSchema, + { supertest = this.supertest, spaceId = DEFAULT_SPACE_ID }: ArtifactCreateOptions = {} ): Promise { - const artifact = await this.supertest - .post(EXCEPTION_LIST_ITEM_URL) + const artifact = await supertest + .post(addSpaceIdToPath('/', spaceId, EXCEPTION_LIST_ITEM_URL)) .set('kbn-xsrf', 'true') .send(createPayload) .then(this.getHttpResponseFailureHandler()) @@ -71,11 +82,19 @@ export class EndpointArtifactsTestResources extends FtrService { const { item_id: itemId, namespace_type: namespaceType, list_id: listId } = artifact; - this.log.info(`Created exception list item [${listId}]: ${itemId}`); + this.log.info( + `Created exception list item in space [${spaceId}], List ID [${listId}], Item ID ${itemId}` + ); const cleanup = async () => { - const deleteResponse = await this.supertest - .delete(`${EXCEPTION_LIST_ITEM_URL}?item_id=${itemId}&namespace_type=${namespaceType}`) + const deleteResponse = await supertest + .delete( + `${addSpaceIdToPath( + '/', + spaceId, + EXCEPTION_LIST_ITEM_URL + )}?item_id=${itemId}&namespace_type=${namespaceType}` + ) .set('kbn-xsrf', 'true') .send() .then(this.getHttpResponseFailureHandler([404])); @@ -92,58 +111,65 @@ export class EndpointArtifactsTestResources extends FtrService { } async createTrustedApp( - overrides: Partial = {} + overrides: Partial = {}, + options?: ArtifactCreateOptions ): Promise { - await this.ensureListExists(TRUSTED_APPS_EXCEPTION_LIST_DEFINITION); + await this.ensureListExists(TRUSTED_APPS_EXCEPTION_LIST_DEFINITION, options); const trustedApp = this.exceptionsGenerator.generateTrustedAppForCreate(overrides); - return this.createExceptionItem(trustedApp); + return this.createExceptionItem(trustedApp, options); } async createEventFilter( - overrides: Partial = {} + overrides: Partial = {}, + options?: ArtifactCreateOptions ): Promise { - await this.ensureListExists(EVENT_FILTER_LIST_DEFINITION); + await this.ensureListExists(EVENT_FILTER_LIST_DEFINITION, options); const eventFilter = this.exceptionsGenerator.generateEventFilterForCreate(overrides); - return this.createExceptionItem(eventFilter); + return this.createExceptionItem(eventFilter, options); } async createHostIsolationException( - overrides: Partial = {} + overrides: Partial = {}, + options?: ArtifactCreateOptions ): Promise { - await this.ensureListExists(HOST_ISOLATION_EXCEPTIONS_LIST_DEFINITION); + await this.ensureListExists(HOST_ISOLATION_EXCEPTIONS_LIST_DEFINITION, options); const artifact = this.exceptionsGenerator.generateHostIsolationExceptionForCreate(overrides); - return this.createExceptionItem(artifact); + return this.createExceptionItem(artifact, options); } async createBlocklist( - overrides: Partial = {} + overrides: Partial = {}, + options?: ArtifactCreateOptions ): Promise { - await this.ensureListExists(BLOCKLISTS_LIST_DEFINITION); + await this.ensureListExists(BLOCKLISTS_LIST_DEFINITION, options); const blocklist = this.exceptionsGenerator.generateBlocklistForCreate(overrides); - return this.createExceptionItem(blocklist); + return this.createExceptionItem(blocklist, options); } async createArtifact( listId: (typeof ENDPOINT_ARTIFACT_LIST_IDS)[number], - overrides: Partial = {} - ): Promise { + overrides: Partial = {}, + options?: ArtifactCreateOptions + ): Promise { switch (listId) { case ENDPOINT_ARTIFACT_LISTS.trustedApps.id: { - return this.createTrustedApp(overrides); + return this.createTrustedApp(overrides, options); } case ENDPOINT_ARTIFACT_LISTS.eventFilters.id: { - return this.createEventFilter(overrides); + return this.createEventFilter(overrides, options); } case ENDPOINT_ARTIFACT_LISTS.blocklists.id: { - return this.createBlocklist(overrides); + return this.createBlocklist(overrides, options); } case ENDPOINT_ARTIFACT_LISTS.hostIsolationExceptions.id: { - return this.createHostIsolationException(overrides); + return this.createHostIsolationException(overrides, options); } + default: + throw new Error(`Unexpected list id ${listId}`); } }