[Security Solution] Add Host Isolation API (#98842)

This commit is contained in:
Dan Panzarella 2021-05-10 13:31:11 -04:00 committed by GitHub
parent 16e1414ae0
commit dfe8637c52
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 634 additions and 93 deletions

View file

@ -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`;

View file

@ -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,
},
},
};
}

View file

@ -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()),
}),
};

View file

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

View file

@ -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;

View 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';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { LicenseService } from '../../../common/license/license';
import { LicenseService } from '../../../common/license';
export const licenseService = new LicenseService();

View file

@ -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],

View file

@ -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;

View file

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

View file

@ -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';

View file

@ -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(),

View file

@ -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');
});
});

View file

@ -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,
},
});
};
};

View file

@ -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,
};
}

View file

@ -6,3 +6,4 @@
*/
export * from './artifacts';
export { getAgentIDsForEndpoints } from './lookup_agent';

View file

@ -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);
}

View file

@ -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';

View file

@ -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,

View file

@ -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 {

View file

@ -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,

View file

@ -5,6 +5,6 @@
* 2.0.
*/
import { LicenseService } from '../../../common/license/license';
import { LicenseService } from '../../../common/license';
export const licenseService = new LicenseService();

View file

@ -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;

View file

@ -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,