[Security Solution][Endpoint Exceptions] - Fix bug where endpoint exceptions list not created when expected (#88232)

## Summary

Addresses issue issue 87110

**Issue**
When prepackaged rules were created during the Endpoint Security enrollment flow, the endpoint exceptions list was failing to be created. As a result, when a user navigated to the `Endpoint Security` rule to add an exception it would display errors and not allow a user to add exceptions.
This commit is contained in:
Yara Tercero 2021-01-19 17:36:05 -05:00 committed by GitHub
parent 1db32352b2
commit 5f53b649c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 104 additions and 10 deletions

View file

@ -28,6 +28,7 @@ export class ExceptionListClientMock extends ExceptionListClient {
public findExceptionListItem = jest.fn().mockResolvedValue(getFoundExceptionListItemSchemaMock()); public findExceptionListItem = jest.fn().mockResolvedValue(getFoundExceptionListItemSchemaMock());
public findExceptionList = jest.fn().mockResolvedValue(getFoundExceptionListSchemaMock()); public findExceptionList = jest.fn().mockResolvedValue(getFoundExceptionListSchemaMock());
public createTrustedAppsList = jest.fn().mockResolvedValue(getTrustedAppsListSchemaMock()); public createTrustedAppsList = jest.fn().mockResolvedValue(getTrustedAppsListSchemaMock());
public createEndpointList = jest.fn().mockResolvedValue(getExceptionListSchemaMock());
} }
export const getExceptionListClientMock = (): ExceptionListClient => { export const getExceptionListClientMock = (): ExceptionListClient => {

View file

@ -9,6 +9,7 @@ import {
SavedObjectsServiceStart, SavedObjectsServiceStart,
SavedObjectsClientContract, SavedObjectsClientContract,
} from 'src/core/server'; } from 'src/core/server';
import { ExceptionListClient } from '../../../lists/server';
import { SecurityPluginSetup } from '../../../security/server'; import { SecurityPluginSetup } from '../../../security/server';
import { import {
AgentService, AgentService,
@ -90,6 +91,7 @@ export type EndpointAppContextServiceStartContract = Partial<
registerIngestCallback?: FleetStartContract['registerExternalCallback']; registerIngestCallback?: FleetStartContract['registerExternalCallback'];
savedObjectsStart: SavedObjectsServiceStart; savedObjectsStart: SavedObjectsServiceStart;
licenseService: LicenseService; licenseService: LicenseService;
exceptionListsClient: ExceptionListClient | undefined;
}; };
/** /**
@ -121,7 +123,8 @@ export class EndpointAppContextService {
dependencies.appClientFactory, dependencies.appClientFactory,
dependencies.config.maxTimelineImportExportSize, dependencies.config.maxTimelineImportExportSize,
dependencies.security, dependencies.security,
dependencies.alerts dependencies.alerts,
dependencies.exceptionListsClient
) )
); );

View file

@ -25,11 +25,14 @@ import { Subject } from 'rxjs';
import { ILicense } from '../../../licensing/common/types'; import { ILicense } from '../../../licensing/common/types';
import { EndpointDocGenerator } from '../../common/endpoint/generate_data'; import { EndpointDocGenerator } from '../../common/endpoint/generate_data';
import { ProtectionModes } from '../../common/endpoint/types'; import { ProtectionModes } from '../../common/endpoint/types';
import { getExceptionListClientMock } from '../../../lists/server/services/exception_lists/exception_list_client.mock';
import { ExceptionListClient } from '../../../lists/server';
describe('ingest_integration tests ', () => { describe('ingest_integration tests ', () => {
let endpointAppContextMock: EndpointAppContextServiceStartContract; let endpointAppContextMock: EndpointAppContextServiceStartContract;
let req: KibanaRequest; let req: KibanaRequest;
let ctx: RequestHandlerContext; let ctx: RequestHandlerContext;
const exceptionListClient: ExceptionListClient = getExceptionListClientMock();
const maxTimelineImportExportSize = createMockConfig().maxTimelineImportExportSize; const maxTimelineImportExportSize = createMockConfig().maxTimelineImportExportSize;
let licenseEmitter: Subject<ILicense>; let licenseEmitter: Subject<ILicense>;
let licenseService: LicenseService; let licenseService: LicenseService;
@ -63,7 +66,8 @@ describe('ingest_integration tests ', () => {
endpointAppContextMock.appClientFactory, endpointAppContextMock.appClientFactory,
maxTimelineImportExportSize, maxTimelineImportExportSize,
endpointAppContextMock.security, endpointAppContextMock.security,
endpointAppContextMock.alerts endpointAppContextMock.alerts,
exceptionListClient
); );
const policyConfig = createNewPackagePolicyMock(); // policy config without manifest const policyConfig = createNewPackagePolicyMock(); // policy config without manifest
const newPolicyConfig = await callback(policyConfig, ctx, req); // policy config WITH manifest const newPolicyConfig = await callback(policyConfig, ctx, req); // policy config WITH manifest
@ -140,7 +144,8 @@ describe('ingest_integration tests ', () => {
endpointAppContextMock.appClientFactory, endpointAppContextMock.appClientFactory,
maxTimelineImportExportSize, maxTimelineImportExportSize,
endpointAppContextMock.security, endpointAppContextMock.security,
endpointAppContextMock.alerts endpointAppContextMock.alerts,
exceptionListClient
); );
const policyConfig = createNewPackagePolicyMock(); const policyConfig = createNewPackagePolicyMock();
const newPolicyConfig = await callback(policyConfig, ctx, req); const newPolicyConfig = await callback(policyConfig, ctx, req);
@ -167,7 +172,8 @@ describe('ingest_integration tests ', () => {
endpointAppContextMock.appClientFactory, endpointAppContextMock.appClientFactory,
maxTimelineImportExportSize, maxTimelineImportExportSize,
endpointAppContextMock.security, endpointAppContextMock.security,
endpointAppContextMock.alerts endpointAppContextMock.alerts,
exceptionListClient
); );
const policyConfig = createNewPackagePolicyMock(); const policyConfig = createNewPackagePolicyMock();
const newPolicyConfig = await callback(policyConfig, ctx, req); const newPolicyConfig = await callback(policyConfig, ctx, req);
@ -188,7 +194,8 @@ describe('ingest_integration tests ', () => {
endpointAppContextMock.appClientFactory, endpointAppContextMock.appClientFactory,
maxTimelineImportExportSize, maxTimelineImportExportSize,
endpointAppContextMock.security, endpointAppContextMock.security,
endpointAppContextMock.alerts endpointAppContextMock.alerts,
exceptionListClient
); );
const policyConfig = createNewPackagePolicyMock(); const policyConfig = createNewPackagePolicyMock();
const newPolicyConfig = await callback(policyConfig, ctx, req); const newPolicyConfig = await callback(policyConfig, ctx, req);
@ -199,6 +206,32 @@ describe('ingest_integration tests ', () => {
lastComputed!.toEndpointFormat() lastComputed!.toEndpointFormat()
); );
}); });
test('policy creation succeeds even if endpoint exception list creation fails', async () => {
const mockError = new Error('error creating endpoint list');
const logger = loggingSystemMock.create().get('ingest_integration.test');
const manifestManager = getManifestManagerMock();
const lastComputed = await manifestManager.getLastComputedManifest();
exceptionListClient.createEndpointList = jest.fn().mockRejectedValue(mockError);
const callback = getPackagePolicyCreateCallback(
logger,
manifestManager,
endpointAppContextMock.appClientFactory,
maxTimelineImportExportSize,
endpointAppContextMock.security,
endpointAppContextMock.alerts,
exceptionListClient
);
const policyConfig = createNewPackagePolicyMock();
const newPolicyConfig = await callback(policyConfig, ctx, req);
expect(exceptionListClient.createEndpointList).toHaveBeenCalled();
expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint');
expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory());
expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual(
lastComputed!.toEndpointFormat()
);
});
}); });
describe('when the license is below platinum', () => { describe('when the license is below platinum', () => {
beforeEach(() => { beforeEach(() => {

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { ExceptionListClient } from '../../../lists/server';
import { PluginStartContract as AlertsStartContract } from '../../../alerts/server'; import { PluginStartContract as AlertsStartContract } from '../../../alerts/server';
import { SecurityPluginSetup } from '../../../security/server'; import { SecurityPluginSetup } from '../../../security/server';
import { ExternalCallback } from '../../../fleet/server'; import { ExternalCallback } from '../../../fleet/server';
@ -84,7 +85,8 @@ export const getPackagePolicyCreateCallback = (
appClientFactory: AppClientFactory, appClientFactory: AppClientFactory,
maxTimelineImportExportSize: number, maxTimelineImportExportSize: number,
securitySetup: SecurityPluginSetup, securitySetup: SecurityPluginSetup,
alerts: AlertsStartContract alerts: AlertsStartContract,
exceptionsClient: ExceptionListClient | undefined
): ExternalCallback[1] => { ): ExternalCallback[1] => {
const handlePackagePolicyCreate = async ( const handlePackagePolicyCreate = async (
newPackagePolicy: NewPackagePolicy, newPackagePolicy: NewPackagePolicy,
@ -119,7 +121,8 @@ export const getPackagePolicyCreateCallback = (
appClient, appClient,
alerts.getAlertsClientWithRequest(request), alerts.getAlertsClientWithRequest(request),
frameworkRequest, frameworkRequest,
maxTimelineImportExportSize maxTimelineImportExportSize,
exceptionsClient
); );
} catch (err) { } catch (err) {
logger.error( logger.error(

View file

@ -6,6 +6,7 @@
import { ILegacyScopedClusterClient, SavedObjectsClientContract } from 'kibana/server'; import { ILegacyScopedClusterClient, SavedObjectsClientContract } from 'kibana/server';
import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks'; import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks';
import { listMock } from '../../../lists/server/mocks';
import { securityMock } from '../../../security/server/mocks'; import { securityMock } from '../../../security/server/mocks';
import { alertsMock } from '../../../alerts/server/mocks'; import { alertsMock } from '../../../alerts/server/mocks';
import { xpackMocks } from '../../../../mocks'; import { xpackMocks } from '../../../../mocks';
@ -79,6 +80,7 @@ export const createMockEndpointAppContextServiceStartContract = (): jest.Mocked<
ReturnType<FleetStartContract['registerExternalCallback']>, ReturnType<FleetStartContract['registerExternalCallback']>,
Parameters<FleetStartContract['registerExternalCallback']> Parameters<FleetStartContract['registerExternalCallback']>
>(), >(),
exceptionListsClient: listMock.getExceptionListClient(),
}; };
}; };

View file

@ -15,7 +15,11 @@ import { requestContextMock, serverMock, createMockConfig, mockGetCurrentUser }
import { AddPrepackagedRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema'; import { AddPrepackagedRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema';
import { SecurityPluginSetup } from '../../../../../../security/server'; import { SecurityPluginSetup } from '../../../../../../security/server';
import { installPrepackagedTimelines } from '../../../timeline/routes/utils/install_prepacked_timelines'; import { installPrepackagedTimelines } from '../../../timeline/routes/utils/install_prepacked_timelines';
import { addPrepackedRulesRoute } from './add_prepackaged_rules_route'; import { addPrepackedRulesRoute, createPrepackagedRules } from './add_prepackaged_rules_route';
import { listMock } from '../../../../../../lists/server/mocks';
import { siemMock } from '../../../../mocks';
import { FrameworkRequest } from '../../../framework';
import { ExceptionListClient } from '../../../../../../lists/server';
jest.mock('../../rules/get_prepackaged_rules', () => { jest.mock('../../rules/get_prepackaged_rules', () => {
return { return {
@ -66,9 +70,11 @@ jest.mock('../../../timeline/routes/utils/install_prepacked_timelines', () => {
}); });
describe('add_prepackaged_rules_route', () => { describe('add_prepackaged_rules_route', () => {
const siemMockClient = siemMock.createClient();
let server: ReturnType<typeof serverMock.create>; let server: ReturnType<typeof serverMock.create>;
let { clients, context } = requestContextMock.createTools(); let { clients, context } = requestContextMock.createTools();
let securitySetup: SecurityPluginSetup; let securitySetup: SecurityPluginSetup;
let mockExceptionsClient: ExceptionListClient;
beforeEach(() => { beforeEach(() => {
server = serverMock.create(); server = serverMock.create();
@ -80,6 +86,8 @@ describe('add_prepackaged_rules_route', () => {
authz: {}, authz: {},
} as unknown) as SecurityPluginSetup; } as unknown) as SecurityPluginSetup;
mockExceptionsClient = listMock.getExceptionListClient();
clients.clusterClient.callAsCurrentUser.mockResolvedValue(getNonEmptyIndex()); clients.clusterClient.callAsCurrentUser.mockResolvedValue(getNonEmptyIndex());
clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
@ -271,4 +279,40 @@ describe('add_prepackaged_rules_route', () => {
timelines_updated: 0, timelines_updated: 0,
}); });
}); });
describe('createPrepackagedRules', () => {
test('uses exception lists client from context when available', async () => {
context.lists = {
getExceptionListClient: jest.fn(),
getListClient: jest.fn(),
};
await createPrepackagedRules(
context,
siemMockClient,
clients.alertsClient,
{} as FrameworkRequest,
1200,
mockExceptionsClient
);
expect(mockExceptionsClient.createEndpointList).not.toHaveBeenCalled();
expect(context.lists?.getExceptionListClient).toHaveBeenCalled();
});
test('uses passed in exceptions list client when lists client not available in context', async () => {
const { lists, ...myContext } = context;
await createPrepackagedRules(
myContext,
siemMockClient,
clients.alertsClient,
{} as FrameworkRequest,
1200,
mockExceptionsClient
);
expect(mockExceptionsClient.createEndpointList).toHaveBeenCalled();
});
});
}); });

View file

@ -32,6 +32,8 @@ import { transformError, buildSiemResponse } from '../utils';
import { AlertsClient } from '../../../../../../alerts/server'; import { AlertsClient } from '../../../../../../alerts/server';
import { FrameworkRequest } from '../../../framework'; import { FrameworkRequest } from '../../../framework';
import { ExceptionListClient } from '../../../../../../lists/server';
export const addPrepackedRulesRoute = ( export const addPrepackedRulesRoute = (
router: IRouter, router: IRouter,
config: ConfigType, config: ConfigType,
@ -89,17 +91,22 @@ export const createPrepackagedRules = async (
siemClient: AppClient, siemClient: AppClient,
alertsClient: AlertsClient, alertsClient: AlertsClient,
frameworkRequest: FrameworkRequest, frameworkRequest: FrameworkRequest,
maxTimelineImportExportSize: number maxTimelineImportExportSize: number,
exceptionsClient?: ExceptionListClient
): Promise<PrePackagedRulesAndTimelinesSchema | null> => { ): Promise<PrePackagedRulesAndTimelinesSchema | null> => {
const clusterClient = context.core.elasticsearch.legacy.client; const clusterClient = context.core.elasticsearch.legacy.client;
const savedObjectsClient = context.core.savedObjects.client; const savedObjectsClient = context.core.savedObjects.client;
const exceptionsListClient =
context.lists != null ? context.lists.getExceptionListClient() : exceptionsClient;
if (!siemClient || !alertsClient) { if (!siemClient || !alertsClient) {
throw new PrepackagedRulesError('', 404); throw new PrepackagedRulesError('', 404);
} }
// This will create the endpoint list if it does not exist yet // This will create the endpoint list if it does not exist yet
await context.lists?.getExceptionListClient().createEndpointList(); if (exceptionsListClient != null) {
await exceptionsListClient.createEndpointList();
}
const rulesFromFileSystem = getPrepackagedRules(); const rulesFromFileSystem = getPrepackagedRules();
const prepackagedRules = await getExistingPrepackagedRules({ alertsClient }); const prepackagedRules = await getExistingPrepackagedRules({ alertsClient });

View file

@ -374,6 +374,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
registerIngestCallback, registerIngestCallback,
savedObjectsStart: core.savedObjects, savedObjectsStart: core.savedObjects,
licenseService, licenseService,
exceptionListsClient: this.lists!.getExceptionListClient(savedObjectsClient, 'kibana'),
}); });
this.telemetryEventsSender.start(core, plugins.telemetry, plugins.taskManager); this.telemetryEventsSender.start(core, plugins.telemetry, plugins.taskManager);