[Security solution] [Endpoint] Adds API check for existing policies when saving/creating trusted apps (#106110) (#106975)

* Adds API check for existing policies when saving/creating trusted apps

* Fixes wrong error message

* Fixes tests and replaces policy id by policy name in error message

* Updates error message text

* Address pr comments and remove old comments in code

* Addressed some pr comments and added type to the error response in order to know which kind of response it is

* removed unused imports

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: David Sánchez <davidsansol92@gmail.com>
This commit is contained in:
Kibana Machine 2021-07-28 08:45:33 -04:00 committed by GitHub
parent 9c28da7f73
commit 3e2a915908
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 377 additions and 52 deletions

View file

@ -5,10 +5,12 @@
* 2.0.
*/
import { ResponseErrorAttributes } from 'kibana/server';
export interface ServerApiError {
statusCode: number;
error: string;
message: string;
attributes?: ResponseErrorAttributes | undefined;
}
export interface SecuritySolutionUiConfigType {

View file

@ -66,14 +66,6 @@ export const CreateTrustedAppFlyout = memo<CreateTrustedAppFlyoutProps>(
const dataTestSubj = flyoutProps['data-test-subj'];
const creationErrorsMessage = useMemo<string | undefined>(
() =>
creationErrors
? CREATE_TRUSTED_APP_ERROR[creationErrors.message.replace(/(\[(.*)\]\: )/, '')] ||
creationErrors.message
: undefined,
[creationErrors]
);
const policies = useMemo<CreateTrustedAppFormProps['policies']>(() => {
return {
// Casting is needed due to the use of `Immutable<>` on the return value from the selector above
@ -82,6 +74,24 @@ export const CreateTrustedAppFlyout = memo<CreateTrustedAppFlyoutProps>(
};
}, [isLoadingPolicies, policyList]);
const creationErrorsMessage = useMemo<string | undefined>(() => {
let errorMessage = creationErrors
? CREATE_TRUSTED_APP_ERROR[creationErrors.message.replace(/(\[(.*)\]\: )/, '')] ||
creationErrors.message
: undefined;
if (
creationErrors &&
creationErrors.attributes &&
creationErrors.attributes.type === 'TrustedApps/PolicyNotFound'
) {
policies.options.forEach((policy) => {
errorMessage = errorMessage?.replace(policy.id, policy.name);
});
}
return errorMessage;
}, [creationErrors, policies]);
const getTestId = useTestIdGenerator(dataTestSubj);
const handleCancelClick = useCallback(() => {

View file

@ -12,6 +12,17 @@ export class TrustedAppNotFoundError extends Error {
super(`Trusted Application (${id}) not found`);
}
}
export class TrustedAppPolicyNotExistsError extends Error {
public readonly type = 'TrustedApps/PolicyNotFound';
constructor(name: string, policyIds: string[]) {
super(
`Trusted Application (${name}) is assigned with a policy that no longer exists: ${policyIds.join(
', '
)}`
);
}
}
export class TrustedAppVersionConflictError extends Error {
constructor(id: string, public sourceError: Error) {

View file

@ -5,10 +5,14 @@
* 2.0.
*/
import { KibanaResponseFactory } from 'kibana/server';
import { KibanaResponseFactory, SavedObjectsClientContract } from 'kibana/server';
import { xpackMocks } from '../../../fixtures';
import { loggingSystemMock, httpServerMock } from '../../../../../../../src/core/server/mocks';
import {
loggingSystemMock,
httpServerMock,
savedObjectsClientMock,
} from '../../../../../../../src/core/server/mocks';
import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { listMock } from '../../../../../lists/server/mocks';
import { ExceptionListClient } from '../../../../../lists/server';
@ -32,9 +36,16 @@ import {
getTrustedAppsUpdateRouteHandler,
} from './handlers';
import type { SecuritySolutionRequestHandlerContext } from '../../../types';
import { TrustedAppNotFoundError, TrustedAppVersionConflictError } from './errors';
import {
TrustedAppNotFoundError,
TrustedAppVersionConflictError,
TrustedAppPolicyNotExistsError,
} from './errors';
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';
const EXCEPTION_LIST_ITEM: ExceptionListItemSchema = {
_version: 'abc123',
@ -88,7 +99,14 @@ const TRUSTED_APP: TrustedApp = {
],
};
const packagePolicyClient = createPackagePolicyServiceMock() as jest.Mocked<PackagePolicyServiceInterface>;
const savedObjectClient = savedObjectsClientMock.create() as jest.Mocked<SavedObjectsClientContract>;
describe('handlers', () => {
beforeEach(() => {
packagePolicyClient.getByIDs.mockReset();
});
const createAppContextMock = () => {
const context = {
logFactory: loggingSystemMock.create(),
@ -97,6 +115,9 @@ describe('handlers', () => {
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
};
context.service.getPackagePolicyService = () => packagePolicyClient;
context.service.getScopedSavedObjectsClient = () => savedObjectClient;
// Ensure that `logFactory.get()` always returns the same instance for the same given prefix
const instances = new Map<string, ReturnType<typeof context.logFactory.get>>();
const logFactoryGetMock = context.logFactory.get.getMockImplementation();
@ -224,6 +245,28 @@ describe('handlers', () => {
)
).rejects.toThrowError(error);
});
it("should return error when policy doesn't exists", async () => {
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 TrustedAppPolicyNotExistsError(trustedAppByPolicy.name, [
'9da95be9-9bee-4761-a8c4-28d6d9bd8c71',
]);
expect(appContextMock.logFactory.get('trusted_apps').error).toHaveBeenCalledWith(error);
expect(mockResponse.badRequest).toHaveBeenCalledWith({
body: { message: error.message, attributes: { type: error.type } },
});
});
});
describe('getTrustedAppsListRouteHandler', () => {
@ -508,5 +551,23 @@ describe('handlers', () => {
body: expect.any(TrustedAppVersionConflictError),
});
});
it("should return error when policy doesn't exists", async () => {
packagePolicyClient.getByIDs.mockReset();
packagePolicyClient.getByIDs.mockResolvedValueOnce(getPackagePoliciesResponse());
const trustedAppByPolicy = getTrustedAppByPolicy();
await updateHandler(
createHandlerContextMock(),
httpServerMock.createKibanaRequest({ body: trustedAppByPolicy }),
mockResponse
);
expect(appContextMock.logFactory.get('trusted_apps').error).toHaveBeenCalledWith(
new TrustedAppPolicyNotExistsError(trustedAppByPolicy.name, [
'9da95be9-9bee-4761-a8c4-28d6d9bd8c71',
])
);
});
});
});

View file

@ -28,7 +28,12 @@ import {
getTrustedAppsSummary,
updateTrustedApp,
} from './service';
import { TrustedAppNotFoundError, TrustedAppVersionConflictError } from './errors';
import {
TrustedAppNotFoundError,
TrustedAppVersionConflictError,
TrustedAppPolicyNotExistsError,
} from './errors';
import { PackagePolicyServiceInterface } from '../../../../../fleet/server';
const getBodyAfterFeatureFlagCheck = (
body: PutTrustedAppUpdateRequest | PostTrustedAppCreateRequest,
@ -54,6 +59,18 @@ const exceptionListClientFromContext = (
return exceptionLists;
};
const packagePolicyClientFromEndpointContext = (
endpointAppContext: EndpointAppContext
): PackagePolicyServiceInterface => {
const packagePolicy = endpointAppContext.service.getPackagePolicyService();
if (!packagePolicy) {
throw new Error('Package policy service not found');
}
return packagePolicy;
};
const errorHandler = <E extends Error>(
logger: Logger,
res: KibanaResponseFactory,
@ -64,6 +81,11 @@ const errorHandler = <E extends Error>(
return res.notFound({ body: error });
}
if (error instanceof TrustedAppPolicyNotExistsError) {
logger.error(error);
return res.badRequest({ body: { message: error.message, attributes: { type: error.type } } });
}
if (error instanceof TrustedAppVersionConflictError) {
logger.error(error);
return res.conflict({ body: error });
@ -150,7 +172,12 @@ export const getTrustedAppsCreateRouteHandler = (
const body = getBodyAfterFeatureFlagCheck(req.body, endpointAppContext);
return res.ok({
body: await createTrustedApp(exceptionListClientFromContext(context), body),
body: await createTrustedApp(
exceptionListClientFromContext(context),
context.core.savedObjects.client,
packagePolicyClientFromEndpointContext(endpointAppContext),
body
),
});
} catch (error) {
return errorHandler(logger, res, error);
@ -173,7 +200,13 @@ export const getTrustedAppsUpdateRouteHandler = (
const body = getBodyAfterFeatureFlagCheck(req.body, endpointAppContext);
return res.ok({
body: await updateTrustedApp(exceptionListClientFromContext(context), req.params.id, body),
body: await updateTrustedApp(
exceptionListClientFromContext(context),
context.core.savedObjects.client,
packagePolicyClientFromEndpointContext(endpointAppContext),
req.params.id,
body
),
});
} catch (error) {
return errorHandler(logger, res, error);

View file

@ -0,0 +1,62 @@
/*
* 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 { PackagePolicy } from '../../../../../fleet/common';
import {
ConditionEntryField,
OperatingSystem,
TrustedApp,
} from '../../../../common/endpoint/types';
import { createConditionEntry } from './mapping';
export const getTrustedAppByPolicy = function (): TrustedApp {
return {
id: '123',
version: 'abc123',
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: OperatingSystem.LINUX,
effectScope: {
type: 'policy',
policies: ['e5cbb9cf-98aa-4303-a04b-6a1165915079', '9da95be9-9bee-4761-a8c4-28d6d9bd8c71'],
},
entries: [
createConditionEntry(ConditionEntryField.HASH, 'match', '1234234659af249ddf3e40864e9fb241'),
createConditionEntry(ConditionEntryField.PATH, 'match', '/bin/malware'),
],
};
};
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.
// @ts-ignore
{ id: '9da95be9-9bee-4761-a8c4-28d6d9bd8c71', version: undefined },
{
id: 'e5cbb9cf-98aa-4303-a04b-6a1165915079',
version: 'Wzc0NDk5LDFd',
name: 'EI 2',
description: '',
namespace: 'default',
policy_id: '9fd2ac50-e86f-11eb-a87f-51e16104076a',
enabled: true,
output_id: '',
inputs: [],
package: { name: 'endpoint', title: 'Endpoint Security', version: '0.20.1' },
revision: 3,
created_at: '2021-07-19T09:00:45.608Z',
created_by: 'elastic',
updated_at: '2021-07-19T09:02:47.193Z',
updated_by: 'system',
},
];
};

View file

@ -7,9 +7,14 @@
import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { listMock } from '../../../../../lists/server/mocks';
import { createPackagePolicyServiceMock } from '../../../../../fleet/server/mocks';
import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks';
import { PackagePolicyServiceInterface } from '../../../../../fleet/server';
import type { SavedObjectsClientContract } from 'kibana/server';
import { ExceptionListClient } from '../../../../../lists/server';
import {
ConditionEntryField,
MaybeImmutable,
OperatingSystem,
TrustedApp,
} from '../../../../common/endpoint/types';
@ -22,12 +27,19 @@ import {
getTrustedAppsSummary,
updateTrustedApp,
} from './service';
import { TrustedAppNotFoundError, TrustedAppVersionConflictError } from './errors';
import {
TrustedAppNotFoundError,
TrustedAppVersionConflictError,
TrustedAppPolicyNotExistsError,
} from './errors';
import { toUpdateTrustedApp } from '../../../../common/endpoint/service/trusted_apps/to_update_trusted_app';
import { updateExceptionListItemImplementationMock } from './test_utils';
import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants';
import { getPackagePoliciesResponse, getTrustedAppByPolicy } from './mocks';
const exceptionsListClient = listMock.getExceptionListClient() as jest.Mocked<ExceptionListClient>;
const packagePolicyClient = createPackagePolicyServiceMock() as jest.Mocked<PackagePolicyServiceInterface>;
const savedObjectClient = savedObjectsClientMock.create() as jest.Mocked<SavedObjectsClientContract>;
const EXCEPTION_LIST_ITEM: ExceptionListItemSchema = {
_version: 'abc123',
@ -76,6 +88,7 @@ describe('service', () => {
exceptionsListClient.createExceptionListItem.mockReset();
exceptionsListClient.findExceptionListItem.mockReset();
exceptionsListClient.createTrustedAppsList.mockReset();
packagePolicyClient.getByIDs.mockReset();
});
describe('deleteTrustedApp', () => {
@ -103,20 +116,25 @@ describe('service', () => {
it('should create trusted app', async () => {
exceptionsListClient.createExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM);
const result = await createTrustedApp(exceptionsListClient, {
name: 'linux trusted app 1',
description: 'Linux trusted app 1',
effectScope: { type: 'global' },
os: OperatingSystem.LINUX,
entries: [
createConditionEntry(ConditionEntryField.PATH, 'match', '/bin/malware'),
createConditionEntry(
ConditionEntryField.HASH,
'match',
'1234234659af249ddf3e40864e9fb241'
),
],
});
const result = await createTrustedApp(
exceptionsListClient,
savedObjectClient,
packagePolicyClient,
{
name: 'linux trusted app 1',
description: 'Linux trusted app 1',
effectScope: { type: 'global' },
os: OperatingSystem.LINUX,
entries: [
createConditionEntry(ConditionEntryField.PATH, 'match', '/bin/malware'),
createConditionEntry(
ConditionEntryField.HASH,
'match',
'1234234659af249ddf3e40864e9fb241'
),
],
}
);
expect(result).toEqual({ data: TRUSTED_APP });
@ -126,25 +144,57 @@ describe('service', () => {
it('should create trusted app with correct wildcard type', async () => {
exceptionsListClient.createExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM);
const result = await createTrustedApp(exceptionsListClient, {
name: 'linux trusted app 1',
description: 'Linux trusted app 1',
effectScope: { type: 'global' },
os: OperatingSystem.LINUX,
entries: [
createConditionEntry(ConditionEntryField.PATH, 'wildcard', '/bin/malware'),
createConditionEntry(
ConditionEntryField.HASH,
'wildcard',
'1234234659af249ddf3e40864e9fb241'
),
],
});
const result = await createTrustedApp(
exceptionsListClient,
savedObjectClient,
packagePolicyClient,
{
name: 'linux trusted app 1',
description: 'Linux trusted app 1',
effectScope: { type: 'global' },
os: OperatingSystem.LINUX,
entries: [
createConditionEntry(ConditionEntryField.PATH, 'wildcard', '/bin/malware'),
createConditionEntry(
ConditionEntryField.HASH,
'wildcard',
'1234234659af249ddf3e40864e9fb241'
),
],
}
);
expect(result).toEqual({ data: TRUSTED_APP });
expect(exceptionsListClient.createTrustedAppsList).toHaveBeenCalled();
});
it("should throw wrong policy error if some policy doesn't exists", 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'
),
],
})
).rejects.toBeInstanceOf(TrustedAppPolicyNotExistsError);
});
});
describe('getTrustedAppsList', () => {
@ -251,7 +301,13 @@ describe('service', () => {
trustedAppForUpdate.entries = [trustedAppForUpdate.entries[0]];
await expect(
updateTrustedApp(exceptionsListClient, TRUSTED_APP.id, trustedAppForUpdate)
updateTrustedApp(
exceptionsListClient,
savedObjectClient,
packagePolicyClient,
TRUSTED_APP.id,
trustedAppForUpdate
)
).resolves.toEqual({
data: {
created_at: '11/11/2011T11:11:11.111',
@ -281,7 +337,13 @@ describe('service', () => {
it('should throw a Not Found error if trusted app is not found prior to making update', async () => {
exceptionsListClient.getExceptionListItem.mockResolvedValueOnce(null);
await expect(
updateTrustedApp(exceptionsListClient, TRUSTED_APP.id, toUpdateTrustedApp(TRUSTED_APP))
updateTrustedApp(
exceptionsListClient,
savedObjectClient,
packagePolicyClient,
TRUSTED_APP.id,
toUpdateTrustedApp(TRUSTED_APP)
)
).rejects.toBeInstanceOf(TrustedAppNotFoundError);
});
@ -292,7 +354,13 @@ describe('service', () => {
);
await expect(
updateTrustedApp(exceptionsListClient, TRUSTED_APP.id, toUpdateTrustedApp(TRUSTED_APP))
updateTrustedApp(
exceptionsListClient,
savedObjectClient,
packagePolicyClient,
TRUSTED_APP.id,
toUpdateTrustedApp(TRUSTED_APP)
)
).rejects.toBeInstanceOf(TrustedAppVersionConflictError);
});
@ -305,9 +373,30 @@ describe('service', () => {
exceptionsListClient.getExceptionListItem.mockResolvedValueOnce(null);
await expect(
updateTrustedApp(exceptionsListClient, TRUSTED_APP.id, toUpdateTrustedApp(TRUSTED_APP))
updateTrustedApp(
exceptionsListClient,
savedObjectClient,
packagePolicyClient,
TRUSTED_APP.id,
toUpdateTrustedApp(TRUSTED_APP)
)
).rejects.toBeInstanceOf(TrustedAppNotFoundError);
});
it("should throw wrong policy error if some policy doesn't exists", async () => {
packagePolicyClient.getByIDs.mockReset();
packagePolicyClient.getByIDs.mockResolvedValueOnce(getPackagePoliciesResponse());
const trustedAppByPolicy = getTrustedAppByPolicy();
await expect(
updateTrustedApp(
exceptionsListClient,
savedObjectClient,
packagePolicyClient,
trustedAppByPolicy.id,
toUpdateTrustedApp(trustedAppByPolicy as MaybeImmutable<TrustedApp>)
)
).rejects.toBeInstanceOf(TrustedAppPolicyNotExistsError);
});
});
describe('getTrustedApp', () => {

View file

@ -5,8 +5,10 @@
* 2.0.
*/
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 { ExceptionListClient } from '../../../../../lists/server';
import {
@ -27,7 +29,38 @@ import {
osFromExceptionItem,
updatedTrustedAppToUpdateExceptionListItemOptions,
} from './mapping';
import { TrustedAppNotFoundError, TrustedAppVersionConflictError } from './errors';
import {
TrustedAppNotFoundError,
TrustedAppVersionConflictError,
TrustedAppPolicyNotExistsError,
} from './errors';
import { PackagePolicyServiceInterface } from '../../../../../fleet/server';
import { PackagePolicy } from '../../../../../fleet/common';
const getNonExistingPoliciesFromTrustedApp = async (
savedObjectClient: SavedObjectsClientContract,
packagePolicyClient: PackagePolicyServiceInterface,
trustedApp: PutTrustedAppUpdateRequest | PostTrustedAppCreateRequest
): Promise<PackagePolicy[]> => {
if (
!trustedApp.effectScope ||
trustedApp.effectScope.type === 'global' ||
(trustedApp.effectScope.type === 'policy' && isEmpty(trustedApp.effectScope.policies))
) {
return [];
}
const policies = await packagePolicyClient.getByIDs(
savedObjectClient,
trustedApp.effectScope.policies
);
if (!policies) {
return [];
}
return policies.filter((policy) => policy.version === undefined);
};
export const deleteTrustedApp = async (
exceptionsListClient: ExceptionListClient,
@ -90,13 +123,25 @@ export const getTrustedAppsList = async (
export const createTrustedApp = async (
exceptionsListClient: ExceptionListClient,
savedObjectClient: SavedObjectsClientContract,
packagePolicyClient: PackagePolicyServiceInterface,
newTrustedApp: PostTrustedAppCreateRequest
): Promise<PostTrustedAppCreateResponse> => {
// Ensure list is created if it does not exist
await exceptionsListClient.createTrustedAppsList();
// Validate update TA entry - error if not valid
// TODO: implement validations
const unexistingPolicies = await getNonExistingPoliciesFromTrustedApp(
savedObjectClient,
packagePolicyClient,
newTrustedApp
);
if (!isEmpty(unexistingPolicies)) {
throw new TrustedAppPolicyNotExistsError(
newTrustedApp.name,
unexistingPolicies.map((policy) => policy.id)
);
}
const createdTrustedAppExceptionItem = await exceptionsListClient.createExceptionListItem(
newTrustedAppToCreateExceptionListItemOptions(newTrustedApp)
@ -107,6 +152,8 @@ export const createTrustedApp = async (
export const updateTrustedApp = async (
exceptionsListClient: ExceptionListClient,
savedObjectClient: SavedObjectsClientContract,
packagePolicyClient: PackagePolicyServiceInterface,
id: string,
updatedTrustedApp: PutTrustedAppUpdateRequest
): Promise<PutTrustedAppUpdateResponse> => {
@ -120,8 +167,18 @@ export const updateTrustedApp = async (
throw new TrustedAppNotFoundError(id);
}
// Validate update TA entry - error if not valid
// TODO: implement validations
const unexistingPolicies = await getNonExistingPoliciesFromTrustedApp(
savedObjectClient,
packagePolicyClient,
updatedTrustedApp
);
if (!isEmpty(unexistingPolicies)) {
throw new TrustedAppPolicyNotExistsError(
updatedTrustedApp.name,
unexistingPolicies.map((policy) => policy.id)
);
}
let updatedTrustedAppExceptionItem: ExceptionListItemSchema | null;