Skip to content

Commit

Permalink
[Security Solution][Endpoint] Add the space ID to endpoint artifacts …
Browse files Browse the repository at this point in the history
…when creating or updating them (#210698)

## Summary

Changes in this PR are in associated with Endpoint Management support
for spaces:

- When creating an endpoint artifact (Trusted Apps, Event Filters,
Blocklists, Host Isolation Exceptions, Endpoint Exceptions), the API
will ensure a new `tag` is dded to each item created that identifies the
space ID from where it was crated
- This functionality is behind the following feature flag:
`endpointManagementSpaceAwarenessEnabled`
- The tag that will be automatically added has a format of:
`ownerSpaceId:<space_id_here>`
- Likewise, when updating an artifact, the API will ensure that at least
1 owner space id tag is present on the item, and if not, it will add one
to it.
  • Loading branch information
paul-tavares authored Feb 18, 2025
1 parent 9a6a349 commit b6f0cc7
Show file tree
Hide file tree
Showing 14 changed files with 322 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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')] });
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Pick<ExceptionListItemSchema, 'tags'>>
): 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<Pick<ExceptionListItemSchema, 'tags'>>,
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<Pick<ExceptionListItemSchema, 'tags'>>
): boolean => {
return (item.tags ?? []).some((tag) => tag.startsWith(OWNER_SPACE_ID_TAG_PREFIX));
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -88,6 +90,7 @@ export interface EndpointAppContextServiceStartContract {
savedObjectsServiceStart: SavedObjectsServiceStart;
connectorActions: ActionsPluginStartContract;
telemetryConfigProvider: TelemetryConfigProvider;
spacesService: SpacesServiceStart | undefined;
}

/**
Expand Down Expand Up @@ -430,4 +433,12 @@ export class EndpointAppContextService {
}
return this.setupDependencies.telemetry;
}

public getActiveSpace(httpRequest: KibanaRequest): Promise<Space> {
if (!this.startDependencies?.spacesService) {
throw new EndpointAppContentServicesNotStartedError();
}

return this.startDependencies.spacesService.getActiveSpace(httpRequest);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<EndpointAppContextService>;
};

Expand Down Expand Up @@ -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<SecurityServiceStart>;
Expand Down Expand Up @@ -218,6 +221,7 @@ export const createMockEndpointAppContextServiceStartContract =
getUnsecuredActionsClient: jest.fn().mockReturnValue(unsecuredActionsClientMock.create()),
} as unknown as jest.Mocked<ActionPluginStartContract>,
telemetryConfigProvider: createTelemetryConfigProviderMock(),
spacesService,
};

return startContract;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<Pick<ExceptionListItemSchema, 'tags'>>
): Promise<void> {
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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -243,6 +244,8 @@ export class BlocklistValidator extends BaseValidator {
await this.validateCanCreateByPolicyArtifacts(item);
await this.validateByPolicyItem(item);

await this.setOwnerSpaceId(item);

return item;
}

Expand Down Expand Up @@ -297,6 +300,10 @@ export class BlocklistValidator extends BaseValidator {

await this.validateByPolicyItem(updatedItem);

if (!hasArtifactOwnerSpaceId(_updatedItem)) {
await this.setOwnerSpaceId(_updatedItem);
}

return _updatedItem;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -59,6 +60,8 @@ export class EventFilterValidator extends BaseValidator {
await this.validateByPolicyItem(item);
}

await this.setOwnerSpaceId(item);

return item;
}

Expand All @@ -83,6 +86,11 @@ export class EventFilterValidator extends BaseValidator {
}

await this.validateByPolicyItem(updatedItem);

if (!hasArtifactOwnerSpaceId(_updatedItem)) {
await this.setOwnerSpaceId(_updatedItem);
}

return _updatedItem;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -79,6 +80,8 @@ export class HostIsolationExceptionsValidator extends BaseValidator {
await this.validateHostIsolationData(item);
await this.validateByPolicyItem(item);

await this.setOwnerSpaceId(item);

return item;
}

Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -207,6 +208,8 @@ export class TrustedAppValidator extends BaseValidator {
await this.validateCanCreateByPolicyArtifacts(item);
await this.validateByPolicyItem(item);

await this.setOwnerSpaceId(item);

return item;
}

Expand Down Expand Up @@ -256,6 +259,10 @@ export class TrustedAppValidator extends BaseValidator {

await this.validateByPolicyItem(updatedItem);

if (!hasArtifactOwnerSpaceId(_updatedItem)) {
await this.setOwnerSpaceId(_updatedItem);
}

return _updatedItem;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading

0 comments on commit b6f0cc7

Please sign in to comment.