[Security Solution] Add Host Isolation API (#98842)
This commit is contained in:
parent
16e1414ae0
commit
dfe8637c52
24 changed files with 634 additions and 93 deletions
|
@ -27,4 +27,5 @@ export const BASE_POLICY_ROUTE = `/api/endpoint/policy`;
|
|||
export const AGENT_POLICY_SUMMARY_ROUTE = `${BASE_POLICY_ROUTE}/summaries`;
|
||||
|
||||
/** Host Isolation Routes */
|
||||
export const HOST_ISOLATION_CREATE_API = `/api/endpoint/isolate`;
|
||||
export const ISOLATE_HOST_ROUTE = `/api/endpoint/isolate`;
|
||||
export const UNISOLATE_HOST_ROUTE = `/api/endpoint/unisolate`;
|
||||
|
|
|
@ -254,6 +254,12 @@ interface HostInfo {
|
|||
version: number;
|
||||
};
|
||||
};
|
||||
configuration: {
|
||||
isolation: boolean;
|
||||
};
|
||||
state: {
|
||||
isolation: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -458,6 +464,12 @@ export class EndpointDocGenerator extends BaseDataGenerator {
|
|||
policy: {
|
||||
applied: this.randomChoice(APPLIED_POLICIES),
|
||||
},
|
||||
configuration: {
|
||||
isolation: false,
|
||||
},
|
||||
state: {
|
||||
isolation: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
|
||||
export const HostIsolationRequestSchema = {
|
||||
body: schema.object({
|
||||
agent_ids: schema.nullable(schema.arrayOf(schema.string())),
|
||||
endpoint_ids: schema.nullable(schema.arrayOf(schema.string())),
|
||||
alert_ids: schema.nullable(schema.arrayOf(schema.string())),
|
||||
case_ids: schema.nullable(schema.arrayOf(schema.string())),
|
||||
comment: schema.nullable(schema.string()),
|
||||
}),
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type ISOLATION_ACTIONS = 'isolate' | 'unisolate';
|
||||
|
||||
export interface EndpointAction {
|
||||
action_id: string;
|
||||
'@timestamp': string;
|
||||
expiration: string;
|
||||
type: 'INPUT_ACTION';
|
||||
input_type: 'endpoint';
|
||||
agents: string[];
|
||||
user_id: string;
|
||||
data: {
|
||||
command: ISOLATION_ACTIONS;
|
||||
comment?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface HostIsolationResponse {
|
||||
action?: string;
|
||||
message?: string;
|
||||
}
|
|
@ -9,6 +9,7 @@ import { ApplicationStart } from 'kibana/public';
|
|||
import { NewPackagePolicy, PackagePolicy } from '../../../../fleet/common';
|
||||
import { ManifestSchema } from '../schema/manifest';
|
||||
|
||||
export * from './actions';
|
||||
export * from './os';
|
||||
export * from './trusted_apps';
|
||||
|
||||
|
@ -466,6 +467,12 @@ export type HostMetadata = Immutable<{
|
|||
version: number;
|
||||
};
|
||||
};
|
||||
configuration: {
|
||||
isolation?: boolean;
|
||||
};
|
||||
state: {
|
||||
isolation?: boolean;
|
||||
};
|
||||
};
|
||||
agent: {
|
||||
id: string;
|
||||
|
|
8
x-pack/plugins/security_solution/common/license/index.ts
Normal file
8
x-pack/plugins/security_solution/common/license/index.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { LicenseService } from './license';
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { LicenseService } from '../../../common/license/license';
|
||||
import { LicenseService } from '../../../common/license';
|
||||
|
||||
export const licenseService = new LicenseService();
|
||||
|
||||
|
|
|
@ -6,13 +6,14 @@
|
|||
*/
|
||||
|
||||
import { UpdateDocumentByQueryResponse } from 'elasticsearch';
|
||||
import { HostIsolationResponse } from '../../../../../common/endpoint/types';
|
||||
import {
|
||||
DETECTION_ENGINE_QUERY_SIGNALS_URL,
|
||||
DETECTION_ENGINE_SIGNALS_STATUS_URL,
|
||||
DETECTION_ENGINE_INDEX_URL,
|
||||
DETECTION_ENGINE_PRIVILEGES_URL,
|
||||
} from '../../../../../common/constants';
|
||||
import { HOST_ISOLATION_CREATE_API } from '../../../../../common/endpoint/constants';
|
||||
import { ISOLATE_HOST_ROUTE } from '../../../../../common/endpoint/constants';
|
||||
import { KibanaServices } from '../../../../common/lib/kibana';
|
||||
import {
|
||||
BasicSignals,
|
||||
|
@ -21,7 +22,6 @@ import {
|
|||
AlertSearchResponse,
|
||||
AlertsIndex,
|
||||
UpdateAlertStatusProps,
|
||||
HostIsolationResponse,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
|
@ -119,7 +119,7 @@ export const createHostIsolation = async ({
|
|||
agentId: string;
|
||||
comment?: string;
|
||||
}): Promise<HostIsolationResponse> =>
|
||||
KibanaServices.get().http.fetch<HostIsolationResponse>(HOST_ISOLATION_CREATE_API, {
|
||||
KibanaServices.get().http.fetch<HostIsolationResponse>(ISOLATE_HOST_ROUTE, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
agent_ids: [agentId],
|
||||
|
|
|
@ -48,10 +48,6 @@ export interface AlertsIndex {
|
|||
index_mapping_outdated: boolean;
|
||||
}
|
||||
|
||||
export interface HostIsolationResponse {
|
||||
action: string;
|
||||
}
|
||||
|
||||
export interface Privilege {
|
||||
username: string;
|
||||
has_all_requested: boolean;
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
SavedObjectsClientContract,
|
||||
} from 'src/core/server';
|
||||
import { ExceptionListClient } from '../../../lists/server';
|
||||
import { SecurityPluginSetup } from '../../../security/server';
|
||||
import { SecurityPluginStart } from '../../../security/server';
|
||||
import {
|
||||
AgentService,
|
||||
FleetStartContract,
|
||||
|
@ -36,7 +36,7 @@ import { ElasticsearchAssetType } from '../../../fleet/common/types/models';
|
|||
import { metadataTransformPrefix } from '../../common/endpoint/constants';
|
||||
import { AppClientFactory } from '../client';
|
||||
import { ConfigType } from '../config';
|
||||
import { LicenseService } from '../../common/license/license';
|
||||
import { LicenseService } from '../../common/license';
|
||||
import {
|
||||
ExperimentalFeatures,
|
||||
parseExperimentalConfigValue,
|
||||
|
@ -91,7 +91,7 @@ export type EndpointAppContextServiceStartContract = Partial<
|
|||
logger: Logger;
|
||||
manifestManager?: ManifestManager;
|
||||
appClientFactory: AppClientFactory;
|
||||
security: SecurityPluginSetup;
|
||||
security: SecurityPluginStart;
|
||||
alerting: AlertsPluginStartContract;
|
||||
config: ConfigType;
|
||||
registerIngestCallback?: FleetStartContract['registerExternalCallback'];
|
||||
|
@ -112,6 +112,8 @@ export class EndpointAppContextService {
|
|||
private savedObjectsStart: SavedObjectsServiceStart | undefined;
|
||||
private metadataService: MetadataService | undefined;
|
||||
private config: ConfigType | undefined;
|
||||
private license: LicenseService | undefined;
|
||||
public security: SecurityPluginStart | undefined;
|
||||
|
||||
private experimentalFeatures: ExperimentalFeatures | undefined;
|
||||
|
||||
|
@ -123,6 +125,8 @@ export class EndpointAppContextService {
|
|||
this.savedObjectsStart = dependencies.savedObjectsStart;
|
||||
this.metadataService = createMetadataService(dependencies.packageService!);
|
||||
this.config = dependencies.config;
|
||||
this.license = dependencies.licenseService;
|
||||
this.security = dependencies.security;
|
||||
|
||||
this.experimentalFeatures = parseExperimentalConfigValue(this.config.enableExperimental);
|
||||
|
||||
|
@ -180,4 +184,11 @@ export class EndpointAppContextService {
|
|||
}
|
||||
return this.savedObjectsStart.getScopedClient(req, { excludedWrappers: ['security'] });
|
||||
}
|
||||
|
||||
public getLicenseService(): LicenseService {
|
||||
if (!this.license) {
|
||||
throw new Error(`must call start on ${EndpointAppContextService.name} to call getter`);
|
||||
}
|
||||
return this.license;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
loggingSystemMock,
|
||||
savedObjectsServiceMock,
|
||||
} from 'src/core/server/mocks';
|
||||
import { LicenseService } from '../../../../common/license/license';
|
||||
import { LicenseService } from '../../../../common/license';
|
||||
import { createPackagePolicyServiceMock } from '../../../../../fleet/server/mocks';
|
||||
import { PolicyWatcher } from './license_watch';
|
||||
import { ILicense } from '../../../../../licensing/common/types';
|
||||
|
|
|
@ -28,8 +28,7 @@ import { ManifestManager } from './services/artifacts/manifest_manager/manifest_
|
|||
import { getManifestManagerMock } from './services/artifacts/manifest_manager/manifest_manager.mock';
|
||||
import { EndpointAppContext } from './types';
|
||||
import { MetadataRequestContext } from './routes/metadata/handlers';
|
||||
// import { licenseMock } from '../../../licensing/common/licensing.mock';
|
||||
import { LicenseService } from '../../common/license/license';
|
||||
import { LicenseService } from '../../common/license';
|
||||
import { SecuritySolutionRequestHandlerContext } from '../types';
|
||||
import { parseExperimentalConfigValue } from '../../common/experimental_features';
|
||||
|
||||
|
@ -78,7 +77,7 @@ export const createMockEndpointAppContextServiceStartContract = (): jest.Mocked<
|
|||
savedObjectsStart: savedObjectsServiceMock.createStartContract(),
|
||||
manifestManager: getManifestManagerMock(),
|
||||
appClientFactory: factory,
|
||||
security: securityMock.createSetup(),
|
||||
security: securityMock.createStart(),
|
||||
alerting: alertsMock.createStart(),
|
||||
config,
|
||||
licenseService: new LicenseService(),
|
||||
|
|
|
@ -0,0 +1,342 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import {
|
||||
ILegacyClusterClient,
|
||||
KibanaResponseFactory,
|
||||
RequestHandler,
|
||||
RouteConfig,
|
||||
} from 'kibana/server';
|
||||
import {
|
||||
elasticsearchServiceMock,
|
||||
httpServerMock,
|
||||
httpServiceMock,
|
||||
loggingSystemMock,
|
||||
savedObjectsClientMock,
|
||||
} from 'src/core/server/mocks';
|
||||
import { parseExperimentalConfigValue } from '../../../../common/experimental_features';
|
||||
import { SecuritySolutionRequestHandlerContext } from '../../../types';
|
||||
import { EndpointAppContextService } from '../../endpoint_app_context_services';
|
||||
import {
|
||||
createMockEndpointAppContextServiceStartContract,
|
||||
createMockPackageService,
|
||||
createRouteHandlerContext,
|
||||
} from '../../mocks';
|
||||
import { registerHostIsolationRoutes } from './isolation';
|
||||
import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__';
|
||||
import { LicenseService } from '../../../../common/license';
|
||||
import { Subject } from 'rxjs';
|
||||
import { ILicense } from '../../../../../licensing/common/types';
|
||||
import { licenseMock } from '../../../../../licensing/common/licensing.mock';
|
||||
import { License } from '../../../../../licensing/common/license';
|
||||
import {
|
||||
ISOLATE_HOST_ROUTE,
|
||||
UNISOLATE_HOST_ROUTE,
|
||||
metadataTransformPrefix,
|
||||
} from '../../../../common/endpoint/constants';
|
||||
import {
|
||||
EndpointAction,
|
||||
HostIsolationResponse,
|
||||
HostMetadata,
|
||||
} from '../../../../common/endpoint/types';
|
||||
import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data';
|
||||
import { createV2SearchResponse } from '../metadata/support/test_support';
|
||||
import { ElasticsearchAssetType } from '../../../../../fleet/common';
|
||||
|
||||
interface CallRouteInterface {
|
||||
body?: any;
|
||||
idxResponse?: any;
|
||||
searchResponse?: HostMetadata;
|
||||
mockUser?: any;
|
||||
license?: License;
|
||||
}
|
||||
|
||||
const Platinum = licenseMock.createLicense({ license: { type: 'platinum', mode: 'platinum' } });
|
||||
const Gold = licenseMock.createLicense({ license: { type: 'gold', mode: 'gold' } });
|
||||
|
||||
describe('Host Isolation', () => {
|
||||
let endpointAppContextService: EndpointAppContextService;
|
||||
let mockResponse: jest.Mocked<KibanaResponseFactory>;
|
||||
let licenseService: LicenseService;
|
||||
let licenseEmitter: Subject<ILicense>;
|
||||
|
||||
let callRoute: (
|
||||
routePrefix: string,
|
||||
opts: CallRouteInterface
|
||||
) => Promise<jest.Mocked<SecuritySolutionRequestHandlerContext>>;
|
||||
const superUser = {
|
||||
username: 'superuser',
|
||||
roles: ['superuser'],
|
||||
};
|
||||
|
||||
const docGen = new EndpointDocGenerator();
|
||||
|
||||
beforeEach(() => {
|
||||
// instantiate... everything
|
||||
const mockScopedClient = elasticsearchServiceMock.createLegacyScopedClusterClient();
|
||||
const mockClusterClient = elasticsearchServiceMock.createLegacyClusterClient() as jest.Mocked<ILegacyClusterClient>;
|
||||
mockClusterClient.asScoped.mockReturnValue(mockScopedClient);
|
||||
const routerMock = httpServiceMock.createRouter();
|
||||
mockResponse = httpServerMock.createResponseFactory();
|
||||
const startContract = createMockEndpointAppContextServiceStartContract();
|
||||
endpointAppContextService = new EndpointAppContextService();
|
||||
const mockSavedObjectClient = savedObjectsClientMock.create();
|
||||
const mockPackageService = createMockPackageService();
|
||||
mockPackageService.getInstalledEsAssetReferences.mockReturnValue(
|
||||
Promise.resolve([
|
||||
{
|
||||
id: 'logs-endpoint.events.security',
|
||||
type: ElasticsearchAssetType.indexTemplate,
|
||||
},
|
||||
{
|
||||
id: `${metadataTransformPrefix}-0.16.0-dev.0`,
|
||||
type: ElasticsearchAssetType.transform,
|
||||
},
|
||||
])
|
||||
);
|
||||
endpointAppContextService.start({ ...startContract, packageService: mockPackageService });
|
||||
licenseEmitter = new Subject();
|
||||
licenseService = new LicenseService();
|
||||
licenseService.start(licenseEmitter);
|
||||
endpointAppContextService.start({
|
||||
...startContract,
|
||||
licenseService,
|
||||
packageService: mockPackageService,
|
||||
});
|
||||
|
||||
// add the host isolation route handlers to routerMock
|
||||
registerHostIsolationRoutes(routerMock, {
|
||||
logFactory: loggingSystemMock.create(),
|
||||
service: endpointAppContextService,
|
||||
config: () => Promise.resolve(createMockConfig()),
|
||||
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
|
||||
});
|
||||
|
||||
// define a convenience function to execute an API call for a given route, body, and mocked response from ES
|
||||
// it returns the requestContext mock used in the call, to assert internal calls (e.g. the indexed document)
|
||||
callRoute = async (
|
||||
routePrefix: string,
|
||||
{ body, idxResponse, searchResponse, mockUser, license }: CallRouteInterface
|
||||
): Promise<jest.Mocked<SecuritySolutionRequestHandlerContext>> => {
|
||||
const asUser = mockUser ? mockUser : superUser;
|
||||
(startContract.security.authc.getCurrentUser as jest.Mock).mockImplementationOnce(
|
||||
() => asUser
|
||||
);
|
||||
const ctx = createRouteHandlerContext(mockScopedClient, mockSavedObjectClient);
|
||||
const withIdxResp = idxResponse ? idxResponse : { statusCode: 201 };
|
||||
ctx.core.elasticsearch.client.asCurrentUser.index = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => Promise.resolve(withIdxResp));
|
||||
ctx.core.elasticsearch.client.asCurrentUser.search = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve({ body: createV2SearchResponse(searchResponse) })
|
||||
);
|
||||
const withLicense = license ? license : Platinum;
|
||||
licenseEmitter.next(withLicense);
|
||||
const mockRequest = httpServerMock.createKibanaRequest({ body });
|
||||
const [, routeHandler]: [
|
||||
RouteConfig<any, any, any, any>,
|
||||
RequestHandler<any, any, any, any>
|
||||
] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith(routePrefix))!;
|
||||
await routeHandler(ctx, mockRequest, mockResponse);
|
||||
return (ctx as unknown) as jest.Mocked<SecuritySolutionRequestHandlerContext>;
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
endpointAppContextService.stop();
|
||||
licenseService.stop();
|
||||
licenseEmitter.complete();
|
||||
});
|
||||
|
||||
it('errors if no endpoint or agent is provided', async () => {
|
||||
await callRoute(ISOLATE_HOST_ROUTE, {});
|
||||
expect(mockResponse.badRequest).toBeCalled();
|
||||
});
|
||||
it('succeeds when an agent ID is provided', async () => {
|
||||
await callRoute(ISOLATE_HOST_ROUTE, { body: { agent_ids: ['XYZ'] } });
|
||||
expect(mockResponse.ok).toBeCalled();
|
||||
});
|
||||
it('reports elasticsearch errors creating an action', async () => {
|
||||
const ErrMessage = 'something went wrong?';
|
||||
|
||||
await callRoute(ISOLATE_HOST_ROUTE, {
|
||||
body: { agent_ids: ['XYZ'] },
|
||||
idxResponse: {
|
||||
statusCode: 500,
|
||||
body: {
|
||||
result: ErrMessage,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockResponse.ok).not.toBeCalled();
|
||||
const response = mockResponse.customError.mock.calls[0][0];
|
||||
expect(response.statusCode).toEqual(500);
|
||||
expect((response.body as HostIsolationResponse).message).toEqual(ErrMessage);
|
||||
});
|
||||
it('accepts a comment field', async () => {
|
||||
await callRoute(ISOLATE_HOST_ROUTE, { body: { agent_ids: ['XYZ'], comment: 'XYZ' } });
|
||||
expect(mockResponse.ok).toBeCalled();
|
||||
});
|
||||
it('sends the action to the requested agent', async () => {
|
||||
const AgentID = '123-ABC';
|
||||
const ctx = await callRoute(ISOLATE_HOST_ROUTE, {
|
||||
body: { agent_ids: [AgentID] },
|
||||
});
|
||||
const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser
|
||||
.index as jest.Mock).mock.calls[0][0].body;
|
||||
expect(actionDoc.agents).toContain(AgentID);
|
||||
});
|
||||
it('records the user who performed the action to the action record', async () => {
|
||||
const testU = { username: 'testuser', roles: ['superuser'] };
|
||||
const ctx = await callRoute(ISOLATE_HOST_ROUTE, {
|
||||
body: { agent_ids: ['XYZ'] },
|
||||
mockUser: testU,
|
||||
});
|
||||
const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser
|
||||
.index as jest.Mock).mock.calls[0][0].body;
|
||||
expect(actionDoc.user_id).toEqual(testU.username);
|
||||
});
|
||||
it('records the comment in the action payload', async () => {
|
||||
const CommentText = "I am isolating this because it's Friday";
|
||||
const ctx = await callRoute(ISOLATE_HOST_ROUTE, {
|
||||
body: { agent_ids: ['XYZ'], comment: CommentText },
|
||||
});
|
||||
const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser
|
||||
.index as jest.Mock).mock.calls[0][0].body;
|
||||
expect(actionDoc.data.comment).toEqual(CommentText);
|
||||
});
|
||||
it('creates an action and returns its ID', async () => {
|
||||
const ctx = await callRoute(ISOLATE_HOST_ROUTE, {
|
||||
body: { agent_ids: ['XYZ'], comment: 'XYZ' },
|
||||
});
|
||||
const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser
|
||||
.index as jest.Mock).mock.calls[0][0].body;
|
||||
const actionID = actionDoc.action_id;
|
||||
expect(mockResponse.ok).toBeCalled();
|
||||
expect((mockResponse.ok.mock.calls[0][0]?.body as HostIsolationResponse).action).toEqual(
|
||||
actionID
|
||||
);
|
||||
});
|
||||
|
||||
it('succeeds when just an endpoint ID is provided', async () => {
|
||||
await callRoute(ISOLATE_HOST_ROUTE, { body: { endpoint_ids: ['XYZ'] } });
|
||||
expect(mockResponse.ok).toBeCalled();
|
||||
});
|
||||
it('sends the action to the correct agent when endpoint ID is given', async () => {
|
||||
const doc = docGen.generateHostMetadata();
|
||||
const AgentID = doc.elastic.agent.id;
|
||||
|
||||
const ctx = await callRoute(ISOLATE_HOST_ROUTE, {
|
||||
body: { endpoint_ids: ['XYZ'] },
|
||||
searchResponse: doc,
|
||||
});
|
||||
const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser
|
||||
.index as jest.Mock).mock.calls[0][0].body;
|
||||
expect(actionDoc.agents).toContain(AgentID);
|
||||
});
|
||||
it('combines given agent IDs and endpoint IDs', async () => {
|
||||
const doc = docGen.generateHostMetadata();
|
||||
const explicitAgentID = 'XYZ';
|
||||
const lookupAgentID = doc.elastic.agent.id;
|
||||
|
||||
const ctx = await callRoute(ISOLATE_HOST_ROUTE, {
|
||||
body: { agent_ids: [explicitAgentID], endpoint_ids: ['XYZ'] },
|
||||
searchResponse: doc,
|
||||
});
|
||||
const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser
|
||||
.index as jest.Mock).mock.calls[0][0].body;
|
||||
expect(actionDoc.agents).toContain(explicitAgentID);
|
||||
expect(actionDoc.agents).toContain(lookupAgentID);
|
||||
});
|
||||
|
||||
it('sends the isolate command payload from the isolate route', async () => {
|
||||
const ctx = await callRoute(ISOLATE_HOST_ROUTE, {
|
||||
body: { agent_ids: ['XYZ'] },
|
||||
});
|
||||
const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser
|
||||
.index as jest.Mock).mock.calls[0][0].body;
|
||||
expect(actionDoc.data.command).toEqual('isolate');
|
||||
});
|
||||
it('sends the unisolate command payload from the unisolate route', async () => {
|
||||
const ctx = await callRoute(UNISOLATE_HOST_ROUTE, {
|
||||
body: { agent_ids: ['XYZ'] },
|
||||
});
|
||||
const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser
|
||||
.index as jest.Mock).mock.calls[0][0].body;
|
||||
expect(actionDoc.data.command).toEqual('unisolate');
|
||||
});
|
||||
|
||||
describe('License Level', () => {
|
||||
it('allows platinum license levels to isolate hosts', async () => {
|
||||
await callRoute(ISOLATE_HOST_ROUTE, {
|
||||
body: { agent_ids: ['XYZ'] },
|
||||
license: Platinum,
|
||||
});
|
||||
expect(mockResponse.ok).toBeCalled();
|
||||
});
|
||||
it('prohibits license levels less than platinum from isolating hosts', async () => {
|
||||
licenseEmitter.next(Gold);
|
||||
await callRoute(ISOLATE_HOST_ROUTE, {
|
||||
body: { agent_ids: ['XYZ'] },
|
||||
license: Gold,
|
||||
});
|
||||
expect(mockResponse.forbidden).toBeCalled();
|
||||
});
|
||||
it('allows any license level to unisolate', async () => {
|
||||
licenseEmitter.next(Gold);
|
||||
await callRoute(UNISOLATE_HOST_ROUTE, {
|
||||
body: { agent_ids: ['XYZ'] },
|
||||
license: Gold,
|
||||
});
|
||||
expect(mockResponse.ok).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('User Level', () => {
|
||||
it('allows superuser to perform isolation', async () => {
|
||||
const superU = { username: 'foo', roles: ['superuser'] };
|
||||
await callRoute(ISOLATE_HOST_ROUTE, {
|
||||
body: { agent_ids: ['XYZ'] },
|
||||
mockUser: superU,
|
||||
});
|
||||
expect(mockResponse.ok).toBeCalled();
|
||||
});
|
||||
it('allows superuser to perform unisolation', async () => {
|
||||
const superU = { username: 'foo', roles: ['superuser'] };
|
||||
await callRoute(UNISOLATE_HOST_ROUTE, {
|
||||
body: { agent_ids: ['XYZ'] },
|
||||
mockUser: superU,
|
||||
});
|
||||
expect(mockResponse.ok).toBeCalled();
|
||||
});
|
||||
|
||||
it('prohibits non-admin user from performing isolation', async () => {
|
||||
const superU = { username: 'foo', roles: ['user'] };
|
||||
await callRoute(ISOLATE_HOST_ROUTE, {
|
||||
body: { agent_ids: ['XYZ'] },
|
||||
mockUser: superU,
|
||||
});
|
||||
expect(mockResponse.forbidden).toBeCalled();
|
||||
});
|
||||
it('prohibits non-admin user from performing unisolation', async () => {
|
||||
const superU = { username: 'foo', roles: ['user'] };
|
||||
await callRoute(UNISOLATE_HOST_ROUTE, {
|
||||
body: { agent_ids: ['XYZ'] },
|
||||
mockUser: superU,
|
||||
});
|
||||
expect(mockResponse.forbidden).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cases', () => {
|
||||
it.todo('logs a comment to the provided case');
|
||||
it.todo('logs a comment to any cases associated with the given alert');
|
||||
});
|
||||
});
|
|
@ -5,81 +5,145 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
import { IRouter } from 'src/core/server';
|
||||
import moment from 'moment';
|
||||
import { RequestHandler } from 'src/core/server';
|
||||
import uuid from 'uuid';
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import { HostIsolationRequestSchema } from '../../../../common/endpoint/schema/actions';
|
||||
import { ISOLATE_HOST_ROUTE, UNISOLATE_HOST_ROUTE } from '../../../../common/endpoint/constants';
|
||||
import { AGENT_ACTIONS_INDEX } from '../../../../../fleet/common';
|
||||
import { EndpointAction } from '../../../../common/endpoint/types';
|
||||
import {
|
||||
SecuritySolutionPluginRouter,
|
||||
SecuritySolutionRequestHandlerContext,
|
||||
} from '../../../types';
|
||||
import { getAgentIDsForEndpoints } from '../../services';
|
||||
import { EndpointAppContext } from '../../types';
|
||||
|
||||
export const userCanIsolate = (roles: readonly string[] | undefined): boolean => {
|
||||
// only superusers can write to the fleet index (or look up endpoint data to convert endp ID to agent ID)
|
||||
if (!roles || roles.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return roles.includes('superuser');
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers the Host-(un-)isolation routes
|
||||
*/
|
||||
export function registerHostIsolationRoutes(router: IRouter, endpointContext: EndpointAppContext) {
|
||||
export function registerHostIsolationRoutes(
|
||||
router: SecuritySolutionPluginRouter,
|
||||
endpointContext: EndpointAppContext
|
||||
) {
|
||||
// perform isolation
|
||||
router.post(
|
||||
{
|
||||
path: `/api/endpoint/isolate`,
|
||||
validate: {
|
||||
body: schema.object({
|
||||
agent_ids: schema.nullable(schema.arrayOf(schema.string())),
|
||||
endpoint_ids: schema.nullable(schema.arrayOf(schema.string())),
|
||||
alert_ids: schema.nullable(schema.arrayOf(schema.string())),
|
||||
case_ids: schema.nullable(schema.arrayOf(schema.string())),
|
||||
comment: schema.nullable(schema.string()),
|
||||
}),
|
||||
},
|
||||
options: { authRequired: true },
|
||||
path: ISOLATE_HOST_ROUTE,
|
||||
validate: HostIsolationRequestSchema,
|
||||
options: { authRequired: true, tags: ['access:securitySolution'] },
|
||||
},
|
||||
async (context, req, res) => {
|
||||
if (
|
||||
(req.body.agent_ids === null || req.body.agent_ids.length === 0) &&
|
||||
(req.body.endpoint_ids === null || req.body.endpoint_ids.length === 0)
|
||||
) {
|
||||
return res.badRequest({
|
||||
body: {
|
||||
message: 'At least one agent ID or endpoint ID is required',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return res.ok({
|
||||
body: {
|
||||
action: '713085d6-ab45-4e9e-b41d-96563cafdd97',
|
||||
},
|
||||
});
|
||||
}
|
||||
isolationRequestHandler(endpointContext, true)
|
||||
);
|
||||
|
||||
// perform UN-isolate
|
||||
router.post(
|
||||
{
|
||||
path: `/api/endpoint/unisolate`,
|
||||
validate: {
|
||||
body: schema.object({
|
||||
agent_ids: schema.nullable(schema.arrayOf(schema.string())),
|
||||
endpoint_ids: schema.nullable(schema.arrayOf(schema.string())),
|
||||
alert_ids: schema.nullable(schema.arrayOf(schema.string())),
|
||||
case_ids: schema.nullable(schema.arrayOf(schema.string())),
|
||||
comment: schema.nullable(schema.string()),
|
||||
}),
|
||||
},
|
||||
options: { authRequired: true },
|
||||
path: UNISOLATE_HOST_ROUTE,
|
||||
validate: HostIsolationRequestSchema,
|
||||
options: { authRequired: true, tags: ['access:securitySolution'] },
|
||||
},
|
||||
async (context, req, res) => {
|
||||
if (
|
||||
(req.body.agent_ids === null || req.body.agent_ids.length === 0) &&
|
||||
(req.body.endpoint_ids === null || req.body.endpoint_ids.length === 0)
|
||||
) {
|
||||
return res.badRequest({
|
||||
body: {
|
||||
message: 'At least one agent ID or endpoint ID is required',
|
||||
},
|
||||
});
|
||||
}
|
||||
return res.ok({
|
||||
isolationRequestHandler(endpointContext, false)
|
||||
);
|
||||
}
|
||||
|
||||
export const isolationRequestHandler = function (
|
||||
endpointContext: EndpointAppContext,
|
||||
isolate: boolean
|
||||
): RequestHandler<
|
||||
unknown,
|
||||
unknown,
|
||||
TypeOf<typeof HostIsolationRequestSchema.body>,
|
||||
SecuritySolutionRequestHandlerContext
|
||||
> {
|
||||
return async (context, req, res) => {
|
||||
if (
|
||||
(!req.body.agent_ids || req.body.agent_ids.length === 0) &&
|
||||
(!req.body.endpoint_ids || req.body.endpoint_ids.length === 0)
|
||||
) {
|
||||
return res.badRequest({
|
||||
body: {
|
||||
action: '53ba1dd1-58a7-407e-b2a9-6843d9980068',
|
||||
message: 'At least one agent ID or endpoint ID is required',
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// only allow admin users
|
||||
const user = endpointContext.service.security?.authc.getCurrentUser(req);
|
||||
if (!userCanIsolate(user?.roles)) {
|
||||
return res.forbidden({
|
||||
body: {
|
||||
message: 'You do not have permission to perform this action',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// isolation requires plat+
|
||||
if (isolate && !endpointContext.service.getLicenseService()?.isPlatinumPlus()) {
|
||||
return res.forbidden({
|
||||
body: {
|
||||
message: 'Your license level does not allow for this action',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// translate any endpoint_ids into agent_ids
|
||||
let agentIDs = req.body.agent_ids?.slice() || [];
|
||||
if (req.body.endpoint_ids && req.body.endpoint_ids.length > 0) {
|
||||
const newIDs = await getAgentIDsForEndpoints(req.body.endpoint_ids, context, endpointContext);
|
||||
agentIDs = agentIDs.concat(newIDs);
|
||||
}
|
||||
agentIDs = [...new Set(agentIDs)]; // dedupe
|
||||
|
||||
// create an Action ID and dispatch it to ES & Fleet Server
|
||||
const esClient = context.core.elasticsearch.client.asCurrentUser;
|
||||
const actionID = uuid.v4();
|
||||
let result;
|
||||
try {
|
||||
result = await esClient.index({
|
||||
index: AGENT_ACTIONS_INDEX,
|
||||
body: {
|
||||
action_id: actionID,
|
||||
'@timestamp': moment().toISOString(),
|
||||
expiration: moment().add(2, 'weeks').toISOString(),
|
||||
type: 'INPUT_ACTION',
|
||||
input_type: 'endpoint',
|
||||
agents: agentIDs,
|
||||
user_id: user?.username,
|
||||
data: {
|
||||
command: isolate ? 'isolate' : 'unisolate',
|
||||
comment: req.body.comment,
|
||||
},
|
||||
} as EndpointAction,
|
||||
});
|
||||
} catch (e) {
|
||||
return res.customError({
|
||||
statusCode: 500,
|
||||
body: { message: e },
|
||||
});
|
||||
}
|
||||
|
||||
if (result.statusCode !== 201) {
|
||||
return res.customError({
|
||||
statusCode: 500,
|
||||
body: {
|
||||
message: result.body.result,
|
||||
},
|
||||
});
|
||||
}
|
||||
return res.ok({
|
||||
body: {
|
||||
action: actionID,
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
|
|
@ -169,3 +169,29 @@ export function getESQueryHostMetadataByID(
|
|||
index: metadataQueryStrategy.index,
|
||||
};
|
||||
}
|
||||
|
||||
export function getESQueryHostMetadataByIDs(
|
||||
agentIDs: string[],
|
||||
metadataQueryStrategy: MetadataQueryStrategy
|
||||
) {
|
||||
return {
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{ terms: { 'agent.id': agentIDs } },
|
||||
{ terms: { 'HostDetails.agent.id': agentIDs } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
sort: MetadataSortMethod,
|
||||
},
|
||||
index: metadataQueryStrategy.index,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,3 +6,4 @@
|
|||
*/
|
||||
|
||||
export * from './artifacts';
|
||||
export { getAgentIDsForEndpoints } from './lookup_agent';
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { SearchRequest } from '@elastic/elasticsearch/api/types';
|
||||
import { SearchResponse } from 'elasticsearch';
|
||||
import { HostMetadata } from '../../../common/endpoint/types';
|
||||
import { SecuritySolutionRequestHandlerContext } from '../../types';
|
||||
import { getESQueryHostMetadataByIDs } from '../routes/metadata/query_builders';
|
||||
import { EndpointAppContext } from '../types';
|
||||
|
||||
export async function getAgentIDsForEndpoints(
|
||||
endpointIDs: string[],
|
||||
requestHandlerContext: SecuritySolutionRequestHandlerContext,
|
||||
endpointAppContext: EndpointAppContext
|
||||
): Promise<string[]> {
|
||||
const queryStrategy = await endpointAppContext.service
|
||||
?.getMetadataService()
|
||||
?.queryStrategy(requestHandlerContext.core.savedObjects.client);
|
||||
|
||||
const query = getESQueryHostMetadataByIDs(endpointIDs, queryStrategy!);
|
||||
const esClient = requestHandlerContext.core.elasticsearch.client.asCurrentUser;
|
||||
const { body } = await esClient.search<HostMetadata>(query as SearchRequest);
|
||||
const hosts = queryStrategy!.queryResponseToHostListResult(body as SearchResponse<HostMetadata>);
|
||||
|
||||
return hosts.resultList.map((x: HostMetadata): string => x.elastic.agent.id);
|
||||
}
|
|
@ -21,7 +21,7 @@ import { createMockConfig, requestContextMock } from '../lib/detection_engine/ro
|
|||
import { EndpointAppContextServiceStartContract } from '../endpoint/endpoint_app_context_services';
|
||||
import { createMockEndpointAppContextServiceStartContract } from '../endpoint/mocks';
|
||||
import { licenseMock } from '../../../licensing/common/licensing.mock';
|
||||
import { LicenseService } from '../../common/license/license';
|
||||
import { LicenseService } from '../../common/license';
|
||||
import { Subject } from 'rxjs';
|
||||
import { ILicense } from '../../../licensing/common/types';
|
||||
import { EndpointDocGenerator } from '../../common/endpoint/generate_data';
|
||||
|
|
|
@ -8,13 +8,13 @@
|
|||
import { KibanaRequest, Logger, RequestHandlerContext } from 'kibana/server';
|
||||
import { ExceptionListClient } from '../../../lists/server';
|
||||
import { PluginStartContract as AlertsStartContract } from '../../../alerting/server';
|
||||
import { SecurityPluginSetup } from '../../../security/server';
|
||||
import { SecurityPluginStart } from '../../../security/server';
|
||||
import { ExternalCallback } from '../../../fleet/server';
|
||||
import { NewPackagePolicy, UpdatePackagePolicy } from '../../../fleet/common';
|
||||
import { NewPolicyData, PolicyConfig } from '../../common/endpoint/types';
|
||||
import { ManifestManager } from '../endpoint/services';
|
||||
import { AppClientFactory } from '../client';
|
||||
import { LicenseService } from '../../common/license/license';
|
||||
import { LicenseService } from '../../common/license';
|
||||
import { installPrepackagedRules } from './handlers/install_prepackaged_rules';
|
||||
import { createPolicyArtifactManifest } from './handlers/create_policy_artifact_manifest';
|
||||
import { createDefaultPolicy } from './handlers/create_default_policy';
|
||||
|
@ -34,7 +34,7 @@ export const getPackagePolicyCreateCallback = (
|
|||
manifestManager: ManifestManager,
|
||||
appClientFactory: AppClientFactory,
|
||||
maxTimelineImportExportSize: number,
|
||||
securitySetup: SecurityPluginSetup,
|
||||
securityStart: SecurityPluginStart,
|
||||
alerts: AlertsStartContract,
|
||||
licenseService: LicenseService,
|
||||
exceptionsClient: ExceptionListClient | undefined
|
||||
|
@ -58,7 +58,7 @@ export const getPackagePolicyCreateCallback = (
|
|||
appClientFactory,
|
||||
context,
|
||||
request,
|
||||
securitySetup,
|
||||
securityStart,
|
||||
alerts,
|
||||
maxTimelineImportExportSize,
|
||||
exceptionsClient,
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { KibanaRequest, Logger, RequestHandlerContext } from 'kibana/server';
|
||||
import { ExceptionListClient } from '../../../../lists/server';
|
||||
import { PluginStartContract as AlertsStartContract } from '../../../../alerting/server';
|
||||
import { SecurityPluginSetup } from '../../../../security/server';
|
||||
import { SecurityPluginStart } from '../../../../security/server';
|
||||
import { AppClientFactory } from '../../client';
|
||||
import { createDetectionIndex } from '../../lib/detection_engine/routes/index/create_index_route';
|
||||
import { createPrepackagedRules } from '../../lib/detection_engine/routes/rules/add_prepackaged_rules_route';
|
||||
|
@ -19,7 +19,7 @@ export interface InstallPrepackagedRulesProps {
|
|||
appClientFactory: AppClientFactory;
|
||||
context: RequestHandlerContext;
|
||||
request: KibanaRequest;
|
||||
securitySetup: SecurityPluginSetup;
|
||||
securityStart: SecurityPluginStart;
|
||||
alerts: AlertsStartContract;
|
||||
maxTimelineImportExportSize: number;
|
||||
exceptionsClient: ExceptionListClient;
|
||||
|
@ -34,7 +34,7 @@ export const installPrepackagedRules = async ({
|
|||
appClientFactory,
|
||||
context,
|
||||
request,
|
||||
securitySetup,
|
||||
securityStart,
|
||||
alerts,
|
||||
maxTimelineImportExportSize,
|
||||
exceptionsClient,
|
||||
|
@ -46,7 +46,7 @@ export const installPrepackagedRules = async ({
|
|||
// It doesn't have access to SecuritySolutionRequestHandlerContext in runtime.
|
||||
// Muting the error to have green CI.
|
||||
// @ts-expect-error
|
||||
const frameworkRequest = await buildFrameworkRequest(context, securitySetup, request);
|
||||
const frameworkRequest = await buildFrameworkRequest(context, securityStart, request);
|
||||
|
||||
// Create detection index & rules (if necessary). move past any failure, this is just a convenience
|
||||
try {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { Logger } from 'kibana/server';
|
||||
import { isEndpointPolicyValidForLicense } from '../../../common/license/policy_config';
|
||||
import { PolicyConfig } from '../../../common/endpoint/types';
|
||||
import { LicenseService } from '../../../common/license/license';
|
||||
import { LicenseService } from '../../../common/license';
|
||||
|
||||
export const validatePolicyAgainstLicense = (
|
||||
policyConfig: PolicyConfig,
|
||||
|
|
|
@ -5,6 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { LicenseService } from '../../../common/license/license';
|
||||
import { LicenseService } from '../../../common/license';
|
||||
|
||||
export const licenseService = new LicenseService();
|
|
@ -14,14 +14,14 @@ import { schema } from '@kbn/config-schema';
|
|||
import { isObject } from 'lodash/fp';
|
||||
|
||||
import { KibanaRequest } from 'src/core/server';
|
||||
import { SetupPlugins } from '../../../plugin';
|
||||
import { SetupPlugins, StartPlugins } from '../../../plugin';
|
||||
import type { SecuritySolutionRequestHandlerContext } from '../../../types';
|
||||
|
||||
import { FrameworkRequest } from '../../framework';
|
||||
|
||||
export const buildFrameworkRequest = async (
|
||||
context: SecuritySolutionRequestHandlerContext,
|
||||
security: SetupPlugins['security'],
|
||||
security: StartPlugins['security'] | SetupPlugins['security'] | undefined,
|
||||
request: KibanaRequest
|
||||
): Promise<FrameworkRequest> => {
|
||||
const savedObjectsClient = context.core.savedObjects.client;
|
||||
|
|
|
@ -27,7 +27,7 @@ import {
|
|||
PluginSetupContract as AlertingSetup,
|
||||
PluginStartContract as AlertPluginStartContract,
|
||||
} from '../../alerting/server';
|
||||
import { SecurityPluginSetup as SecuritySetup } from '../../security/server';
|
||||
import { SecurityPluginSetup as SecuritySetup, SecurityPluginStart } from '../../security/server';
|
||||
import { PluginSetupContract as FeaturesSetup } from '../../features/server';
|
||||
import { MlPluginSetup as MlSetup } from '../../ml/server';
|
||||
import { ListPluginSetup } from '../../lists/server';
|
||||
|
@ -73,7 +73,7 @@ import {
|
|||
TelemetryPluginStart,
|
||||
TelemetryPluginSetup,
|
||||
} from '../../../../src/plugins/telemetry/server';
|
||||
import { licenseService } from './lib/license/license';
|
||||
import { licenseService } from './lib/license';
|
||||
import { PolicyWatcher } from './endpoint/lib/policy/license_watch';
|
||||
import { securitySolutionTimelineEqlSearchStrategyProvider } from './search_strategy/timeline/eql';
|
||||
import { parseExperimentalConfigValue } from '../common/experimental_features';
|
||||
|
@ -100,6 +100,7 @@ export interface StartPlugins {
|
|||
licensing: LicensingPluginStart;
|
||||
taskManager?: TaskManagerStartContract;
|
||||
telemetry?: TelemetryPluginStart;
|
||||
security: SecurityPluginStart;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
|
@ -132,7 +133,6 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
private readonly config: ConfigType;
|
||||
private context: PluginInitializerContext;
|
||||
private appClientFactory: AppClientFactory;
|
||||
private setupPlugins?: SetupPlugins;
|
||||
private readonly endpointAppContextService = new EndpointAppContextService();
|
||||
private readonly telemetryEventsSender: TelemetryEventsSender;
|
||||
|
||||
|
@ -157,7 +157,6 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
|
||||
public setup(core: CoreSetup<StartPlugins, PluginStart>, plugins: SetupPlugins) {
|
||||
this.logger.debug('plugin setup');
|
||||
this.setupPlugins = plugins;
|
||||
|
||||
const config = this.config;
|
||||
const globalConfig = this.context.config.legacy.get();
|
||||
|
@ -397,7 +396,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
packagePolicyService: plugins.fleet?.packagePolicyService,
|
||||
agentPolicyService: plugins.fleet?.agentPolicyService,
|
||||
appClientFactory: this.appClientFactory,
|
||||
security: this.setupPlugins!.security!,
|
||||
security: plugins.security,
|
||||
alerting: plugins.alerting,
|
||||
config: this.config!,
|
||||
logger,
|
||||
|
|
Loading…
Reference in a new issue