diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_flyout.tsx index 049ab5884b1796..7abf5d77dd5e91 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_flyout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_flyout.tsx @@ -88,6 +88,18 @@ export const CreateTrustedAppFlyout = memo( policies.options.forEach((policy) => { errorMessage = errorMessage?.replace(policy.id, policy.name); }); + } else if ( + creationErrors && + creationErrors.attributes && + creationErrors.attributes.type === 'EndpointLicenseError' + ) { + errorMessage = i18n.translate( + 'xpack.securitySolution.trustedapps.createTrustedAppFlyout.byPolicyLicenseError', + { + defaultMessage: + 'Your Kibana license has been downgraded. As such, individual policy configuration is no longer supported.', + } + ); } return errorMessage; }, [creationErrors, policies]); diff --git a/x-pack/plugins/security_solution/server/endpoint/errors.ts b/x-pack/plugins/security_solution/server/endpoint/errors.ts index 6bd664401b4493..fae15984d9c44c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/errors.ts +++ b/x-pack/plugins/security_solution/server/endpoint/errors.ts @@ -22,3 +22,8 @@ export class EndpointAppContentServicesNotStartedError extends EndpointError { super('EndpointAppContextService has not been started (EndpointAppContextService.start())'); } } +export class EndpointLicenseError extends EndpointError { + constructor() { + super('Your license level does not allow for this action.'); + } +} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/errors.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/errors.ts index 3d03040dd26053..277b19c46178b5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/errors.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/errors.ts @@ -25,7 +25,6 @@ export class TrustedAppPolicyNotExistsError extends Error { ); } } - export class TrustedAppVersionConflictError extends Error { constructor(id: string, public sourceError: Error) { super(`Trusted Application (${id}) has been updated since last retrieved`); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts index 81f1d454715942..547c1f6a2e5ffc 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts @@ -6,6 +6,7 @@ */ import { KibanaResponseFactory } from 'kibana/server'; +import { Subject } from 'rxjs'; import { xpackMocks } from '../../../fixtures'; import { loggingSystemMock, httpServerMock } from '../../../../../../../src/core/server/mocks'; @@ -20,6 +21,9 @@ import { OperatingSystem, TrustedApp, } from '../../../../common/endpoint/types'; +import { LicenseService } from '../../../../common/license'; +import { ILicense } from '../../../../../licensing/common/types'; +import { licenseMock } from '../../../../../licensing/common/licensing.mock'; import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; import { createConditionEntry, createEntryMatch } from './mapping'; @@ -41,7 +45,12 @@ import { updateExceptionListItemImplementationMock } from './test_utils'; import { Logger } from '@kbn/logging'; import { PackagePolicyServiceInterface } from '../../../../../fleet/server'; import { createPackagePolicyServiceMock } from '../../../../../fleet/server/mocks'; -import { getPackagePoliciesResponse, getTrustedAppByPolicy } from './mocks'; +import { + getPackagePoliciesResponse, + getPutTrustedAppByPolicyMock, + getTrustedAppByPolicy, +} from './mocks'; +import { EndpointLicenseError } from '../../errors'; const EXCEPTION_LIST_ITEM: ExceptionListItemSchema = { _version: 'abc123', @@ -95,6 +104,9 @@ const TRUSTED_APP: TrustedApp = { ], }; +const Platinum = licenseMock.createLicense({ license: { type: 'platinum', mode: 'platinum' } }); +const Gold = licenseMock.createLicense({ license: { type: 'gold', mode: 'gold' } }); + const packagePolicyClient = createPackagePolicyServiceMock() as jest.Mocked; @@ -102,6 +114,9 @@ describe('handlers', () => { beforeEach(() => { packagePolicyClient.getByIDs.mockReset(); }); + const licenseEmitter: Subject = new Subject(); + const licenseService = new LicenseService(); + licenseService.start(licenseEmitter); const createAppContextMock = () => { const context = { @@ -112,6 +127,7 @@ describe('handlers', () => { }; context.service.getPackagePolicyService = () => packagePolicyClient; + context.service.getLicenseService = () => licenseService; // Ensure that `logFactory.get()` always returns the same instance for the same given prefix const instances = new Map>(); @@ -151,6 +167,7 @@ describe('handlers', () => { beforeEach(() => { appContextMock = createAppContextMock(); exceptionsListClient = listMock.getExceptionListClient() as jest.Mocked; + licenseEmitter.next(Platinum); }); describe('getTrustedAppsDeleteRouteHandler', () => { @@ -261,6 +278,27 @@ describe('handlers', () => { body: { message: error.message, attributes: { type: error.type } }, }); }); + + it('should return error when license under platinum and by policy', async () => { + licenseEmitter.next(Gold); + const mockResponse = httpServerMock.createResponseFactory(); + packagePolicyClient.getByIDs.mockReset(); + packagePolicyClient.getByIDs.mockResolvedValueOnce(getPackagePoliciesResponse()); + + const trustedAppByPolicy = getTrustedAppByPolicy(); + await createTrustedAppHandler( + createHandlerContextMock(), + httpServerMock.createKibanaRequest({ body: trustedAppByPolicy }), + mockResponse + ); + + const error = new EndpointLicenseError(); + + expect(appContextMock.logFactory.get('trusted_apps').error).toHaveBeenCalledWith(error); + expect(mockResponse.badRequest).toHaveBeenCalledWith({ + body: { message: error.message, attributes: { type: error.name } }, + }); + }); }); describe('getTrustedAppsListRouteHandler', () => { @@ -578,18 +616,57 @@ describe('handlers', () => { packagePolicyClient.getByIDs.mockReset(); packagePolicyClient.getByIDs.mockResolvedValueOnce(getPackagePoliciesResponse()); - const trustedAppByPolicy = getTrustedAppByPolicy(); + const exceptionByPolicy = getPutTrustedAppByPolicyMock(); + const customExceptionListClient = { + ...exceptionsListClient, + getExceptionListItem: () => exceptionByPolicy, + }; + const handlerContextMock = { + ...xpackMocks.createRequestHandlerContext(), + lists: { + getListClient: jest.fn(), + getExceptionListClient: jest.fn().mockReturnValue(customExceptionListClient), + }, + } as unknown as jest.Mocked; await updateHandler( - createHandlerContextMock(), - httpServerMock.createKibanaRequest({ body: trustedAppByPolicy }), + handlerContextMock, + httpServerMock.createKibanaRequest({ body: getTrustedAppByPolicy() }), mockResponse ); expect(appContextMock.logFactory.get('trusted_apps').error).toHaveBeenCalledWith( - new TrustedAppPolicyNotExistsError(trustedAppByPolicy.name, [ + new TrustedAppPolicyNotExistsError(exceptionByPolicy.name, [ '9da95be9-9bee-4761-a8c4-28d6d9bd8c71', ]) ); }); + + it('should return error when license under platinum and by policy', async () => { + licenseEmitter.next(Gold); + packagePolicyClient.getByIDs.mockReset(); + packagePolicyClient.getByIDs.mockResolvedValueOnce(getPackagePoliciesResponse()); + + const exceptionByPolicy = getPutTrustedAppByPolicyMock(); + const customExceptionListClient = { + ...exceptionsListClient, + getExceptionListItem: () => exceptionByPolicy, + }; + const handlerContextMock = { + ...xpackMocks.createRequestHandlerContext(), + lists: { + getListClient: jest.fn(), + getExceptionListClient: jest.fn().mockReturnValue(customExceptionListClient), + }, + } as unknown as jest.Mocked; + await updateHandler( + handlerContextMock, + httpServerMock.createKibanaRequest({ body: getTrustedAppByPolicy() }), + mockResponse + ); + + expect(appContextMock.logFactory.get('trusted_apps').error).toHaveBeenCalledWith( + new EndpointLicenseError() + ); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts index 13282bfacd5b13..b02b9d5430cadf 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts @@ -35,6 +35,7 @@ import { TrustedAppPolicyNotExistsError, } from './errors'; import { PackagePolicyServiceInterface } from '../../../../../fleet/server'; +import { EndpointLicenseError } from '../../errors'; const getBodyAfterFeatureFlagCheck = ( body: PutTrustedAppUpdateRequest | PostTrustedAppCreateRequest, @@ -87,6 +88,11 @@ const errorHandler = ( return res.badRequest({ body: { message: error.message, attributes: { type: error.type } } }); } + if (error instanceof EndpointLicenseError) { + logger.error(error); + return res.badRequest({ body: { message: error.message, attributes: { type: error.name } } }); + } + if (error instanceof TrustedAppVersionConflictError) { logger.error(error); return res.conflict({ body: error }); @@ -177,7 +183,8 @@ export const getTrustedAppsCreateRouteHandler = ( exceptionListClientFromContext(context), context.core.savedObjects.client, packagePolicyClientFromEndpointContext(endpointAppContext), - body + body, + endpointAppContext.service.getLicenseService().isAtLeast('platinum') ), }); } catch (error) { @@ -206,7 +213,8 @@ export const getTrustedAppsUpdateRouteHandler = ( context.core.savedObjects.client, packagePolicyClientFromEndpointContext(endpointAppContext), req.params.id, - body + body, + endpointAppContext.service.getLicenseService().isAtLeast('platinum') ), }); } catch (error) { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mocks.ts index e66c07f2e16270..083263809d3091 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mocks.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import { PackagePolicy } from '../../../../../fleet/common'; import { @@ -36,6 +37,32 @@ export const getTrustedAppByPolicy = function (): TrustedApp { }; }; +export const getPutTrustedAppByPolicyMock = function (): ExceptionListItemSchema { + return { + id: '123', + _version: '1', + comments: [], + namespace_type: 'agnostic', + created_at: '11/11/2011T11:11:11.111', + created_by: 'admin', + updated_at: '11/11/2011T11:11:11.111', + updated_by: 'admin', + name: 'linux trusted app 1', + description: 'Linux trusted app 1', + os_types: [OperatingSystem.LINUX], + tags: ['policy:9da95be9-9bee-4761-a8c4-28d6d9bd8c71'], + entries: [ + createConditionEntry(ConditionEntryField.HASH, 'match', '1234234659af249ddf3e40864e9fb241'), + createConditionEntry(ConditionEntryField.PATH, 'match', '/bin/malware'), + ], + item_id: '1', + list_id: '1', + meta: undefined, + tie_breaker_id: '1', + type: 'simple', + }; +}; + export const getPackagePoliciesResponse = function (): PackagePolicy[] { return [ // Next line is ts-ignored as this is the response when the policy doesn't exists but the type is complaining about it. diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts index fd7baf80983a46..dce84df735929e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts @@ -36,6 +36,7 @@ import { toUpdateTrustedApp } from '../../../../common/endpoint/service/trusted_ import { updateExceptionListItemImplementationMock } from './test_utils'; import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants'; import { getPackagePoliciesResponse, getTrustedAppByPolicy } from './mocks'; +import { EndpointLicenseError } from '../../errors'; const exceptionsListClient = listMock.getExceptionListClient() as jest.Mocked; const packagePolicyClient = @@ -135,7 +136,8 @@ describe('service', () => { '1234234659af249ddf3e40864e9fb241' ), ], - } + }, + true ); expect(result).toEqual({ data: TRUSTED_APP }); @@ -163,7 +165,8 @@ describe('service', () => { '1234234659af249ddf3e40864e9fb241' ), ], - } + }, + true ); expect(result).toEqual({ data: TRUSTED_APP }); @@ -175,28 +178,67 @@ describe('service', () => { packagePolicyClient.getByIDs.mockReset(); packagePolicyClient.getByIDs.mockResolvedValueOnce(getPackagePoliciesResponse()); await expect( - createTrustedApp(exceptionsListClient, savedObjectClient, packagePolicyClient, { - name: 'linux trusted app 1', - description: 'Linux trusted app 1', - effectScope: { - type: 'policy', - policies: [ - 'e5cbb9cf-98aa-4303-a04b-6a1165915079', - '9da95be9-9bee-4761-a8c4-28d6d9bd8c71', + createTrustedApp( + exceptionsListClient, + savedObjectClient, + packagePolicyClient, + { + name: 'linux trusted app 1', + description: 'Linux trusted app 1', + effectScope: { + type: 'policy', + policies: [ + 'e5cbb9cf-98aa-4303-a04b-6a1165915079', + '9da95be9-9bee-4761-a8c4-28d6d9bd8c71', + ], + }, + os: OperatingSystem.LINUX, + entries: [ + createConditionEntry(ConditionEntryField.PATH, 'wildcard', '/bin/malware'), + createConditionEntry( + ConditionEntryField.HASH, + 'wildcard', + '1234234659af249ddf3e40864e9fb241' + ), ], }, - os: OperatingSystem.LINUX, - entries: [ - createConditionEntry(ConditionEntryField.PATH, 'wildcard', '/bin/malware'), - createConditionEntry( - ConditionEntryField.HASH, - 'wildcard', - '1234234659af249ddf3e40864e9fb241' - ), - ], - }) + true + ) ).rejects.toBeInstanceOf(TrustedAppPolicyNotExistsError); }); + + it('should throw when license under platinum and by policy', async () => { + packagePolicyClient.getByIDs.mockReset(); + packagePolicyClient.getByIDs.mockResolvedValueOnce(getPackagePoliciesResponse()); + await expect( + createTrustedApp( + exceptionsListClient, + savedObjectClient, + packagePolicyClient, + { + name: 'linux trusted app 1', + description: 'Linux trusted app 1', + effectScope: { + type: 'policy', + policies: [ + 'e5cbb9cf-98aa-4303-a04b-6a1165915079', + '9da95be9-9bee-4761-a8c4-28d6d9bd8c71', + ], + }, + os: OperatingSystem.LINUX, + entries: [ + createConditionEntry(ConditionEntryField.PATH, 'wildcard', '/bin/malware'), + createConditionEntry( + ConditionEntryField.HASH, + 'wildcard', + '1234234659af249ddf3e40864e9fb241' + ), + ], + }, + false + ) + ).rejects.toBeInstanceOf(EndpointLicenseError); + }); }); describe('getTrustedAppsList', () => { @@ -321,7 +363,8 @@ describe('service', () => { savedObjectClient, packagePolicyClient, TRUSTED_APP.id, - trustedAppForUpdate + trustedAppForUpdate, + true ) ).resolves.toEqual({ data: { @@ -357,7 +400,8 @@ describe('service', () => { savedObjectClient, packagePolicyClient, TRUSTED_APP.id, - toUpdateTrustedApp(TRUSTED_APP) + toUpdateTrustedApp(TRUSTED_APP), + true ) ).rejects.toBeInstanceOf(TrustedAppNotFoundError); }); @@ -374,7 +418,8 @@ describe('service', () => { savedObjectClient, packagePolicyClient, TRUSTED_APP.id, - toUpdateTrustedApp(TRUSTED_APP) + toUpdateTrustedApp(TRUSTED_APP), + true ) ).rejects.toBeInstanceOf(TrustedAppVersionConflictError); }); @@ -393,12 +438,13 @@ describe('service', () => { savedObjectClient, packagePolicyClient, TRUSTED_APP.id, - toUpdateTrustedApp(TRUSTED_APP) + toUpdateTrustedApp(TRUSTED_APP), + true ) ).rejects.toBeInstanceOf(TrustedAppNotFoundError); }); - it("should throw wrong policy error if some policy doesn't exists", async () => { + it("should throw wrong policy error if some policy doesn't exists during update", async () => { packagePolicyClient.getByIDs.mockReset(); packagePolicyClient.getByIDs.mockResolvedValueOnce(getPackagePoliciesResponse()); const trustedAppByPolicy = getTrustedAppByPolicy(); @@ -408,10 +454,27 @@ describe('service', () => { savedObjectClient, packagePolicyClient, trustedAppByPolicy.id, - toUpdateTrustedApp(trustedAppByPolicy as MaybeImmutable) + toUpdateTrustedApp(trustedAppByPolicy as MaybeImmutable), + true ) ).rejects.toBeInstanceOf(TrustedAppPolicyNotExistsError); }); + + it('should throw when license under platinum and by policy', async () => { + packagePolicyClient.getByIDs.mockReset(); + packagePolicyClient.getByIDs.mockResolvedValueOnce(getPackagePoliciesResponse()); + const trustedAppByPolicy = getTrustedAppByPolicy(); + await expect( + updateTrustedApp( + exceptionsListClient, + savedObjectClient, + packagePolicyClient, + trustedAppByPolicy.id, + toUpdateTrustedApp(trustedAppByPolicy as MaybeImmutable), + false + ) + ).rejects.toBeInstanceOf(EndpointLicenseError); + }); }); describe('getTrustedApp', () => { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts index a427f13859f03b..7cbdbceaf24cca 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts @@ -8,7 +8,7 @@ import type { SavedObjectsClientContract } from 'kibana/server'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants'; -import { isEmpty } from 'lodash/fp'; +import { isEmpty, isEqual } from 'lodash/fp'; import { ExceptionListClient } from '../../../../../lists/server'; import { @@ -22,6 +22,7 @@ import { PutTrustedAppUpdateRequest, PutTrustedAppUpdateResponse, GetTrustedAppsSummaryRequest, + TrustedApp, } from '../../../../common/endpoint/types'; import { @@ -37,6 +38,7 @@ import { } from './errors'; import { PackagePolicyServiceInterface } from '../../../../../fleet/server'; import { PackagePolicy } from '../../../../../fleet/common'; +import { EndpointLicenseError } from '../../errors'; const getNonExistingPoliciesFromTrustedApp = async ( savedObjectClient: SavedObjectsClientContract, @@ -63,6 +65,28 @@ const getNonExistingPoliciesFromTrustedApp = async ( return policies.filter((policy) => policy.version === undefined); }; +const isUserTryingToModifyEffectScopeWithoutPermissions = ( + currentTrustedApp: TrustedApp, + updatedTrustedApp: PutTrustedAppUpdateRequest, + isAtLeastPlatinum: boolean +): boolean => { + if (updatedTrustedApp.effectScope.type === 'global') { + return false; + } else if (isAtLeastPlatinum) { + return false; + } else if ( + isEqual( + currentTrustedApp.effectScope.type === 'policy' && + currentTrustedApp.effectScope.policies.sort(), + updatedTrustedApp.effectScope.policies.sort() + ) + ) { + return false; + } else { + return true; + } +}; + export const deleteTrustedApp = async ( exceptionsListClient: ExceptionListClient, { id }: DeleteTrustedAppsRequestParams @@ -126,11 +150,16 @@ export const createTrustedApp = async ( exceptionsListClient: ExceptionListClient, savedObjectClient: SavedObjectsClientContract, packagePolicyClient: PackagePolicyServiceInterface, - newTrustedApp: PostTrustedAppCreateRequest + newTrustedApp: PostTrustedAppCreateRequest, + isAtLeastPlatinum: boolean ): Promise => { // Ensure list is created if it does not exist await exceptionsListClient.createTrustedAppsList(); + if (newTrustedApp.effectScope.type === 'policy' && !isAtLeastPlatinum) { + throw new EndpointLicenseError(); + } + const unexistingPolicies = await getNonExistingPoliciesFromTrustedApp( savedObjectClient, packagePolicyClient, @@ -156,7 +185,8 @@ export const updateTrustedApp = async ( savedObjectClient: SavedObjectsClientContract, packagePolicyClient: PackagePolicyServiceInterface, id: string, - updatedTrustedApp: PutTrustedAppUpdateRequest + updatedTrustedApp: PutTrustedAppUpdateRequest, + isAtLeastPlatinum: boolean ): Promise => { const currentTrustedApp = await exceptionsListClient.getExceptionListItem({ itemId: '', @@ -168,6 +198,16 @@ export const updateTrustedApp = async ( throw new TrustedAppNotFoundError(id); } + if ( + isUserTryingToModifyEffectScopeWithoutPermissions( + exceptionListItemToTrustedApp(currentTrustedApp), + updatedTrustedApp, + isAtLeastPlatinum + ) + ) { + throw new EndpointLicenseError(); + } + const unexistingPolicies = await getNonExistingPoliciesFromTrustedApp( savedObjectClient, packagePolicyClient,