[Security Solution] Interim Host Isolation Case Commenting (#100092)

This commit is contained in:
Dan Panzarella 2021-05-14 14:40:24 -04:00 committed by GitHub
parent 25cad22b3d
commit 97cc6ddb6b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 104 additions and 3 deletions

View file

@ -17,6 +17,7 @@ import {
CasesClientGetUserActions,
CasesClientGetAlerts,
CasesClientPush,
CasesClientGetCasesByAlert,
} from './types';
import { create } from './cases/create';
import { update } from './cases/update';
@ -247,4 +248,16 @@ export class CasesClientHandler implements CasesClient {
});
}
}
public async getCaseIdsByAlertId(args: CasesClientGetCasesByAlert) {
try {
return this._caseService.getCaseIdsByAlertId({
client: this._savedObjectsClient,
alertId: args.alertId,
});
} catch (error) {
this.logger.error(`Failed to get case using alert id: ${args.alertId}: ${error}`);
throw error;
}
}
}

View file

@ -31,6 +31,7 @@ export const createExternalCasesClientMock = (): CasesClientPluginContractMock =
getUserActions: jest.fn(),
update: jest.fn(),
updateAlertsStatus: jest.fn(),
getCaseIdsByAlertId: jest.fn(),
});
export const createCasesClientWithMockSavedObjectsClient = async ({

View file

@ -83,6 +83,10 @@ export interface ConfigureFields {
connectorType: string;
}
export interface CasesClientGetCasesByAlert {
alertId: string;
}
/**
* Defines the fields necessary to update an alert's status.
*/
@ -106,6 +110,7 @@ export interface CasesClient {
push(args: CasesClientPush): Promise<CaseResponse>;
update(args: CasesPatchRequest): Promise<CasesResponse>;
updateAlertsStatus(args: CasesClientUpdateAlertsStatus): Promise<void>;
getCaseIdsByAlertId(args: CasesClientGetCasesByAlert): Promise<string[]>;
}
export interface MappingsClient {

View file

@ -5,7 +5,14 @@
* 2.0.
*/
import { PluginConfigDescriptor, PluginInitializerContext } from 'kibana/server';
import {
KibanaRequest,
PluginConfigDescriptor,
PluginInitializerContext,
RequestHandlerContext,
} from 'kibana/server';
import { CasesClient } from './client';
export { CasesClient } from './client';
import { ConfigType, ConfigSchema } from './config';
import { CasePlugin } from './plugin';
@ -18,3 +25,10 @@ export const config: PluginConfigDescriptor<ConfigType> = {
};
export const plugin = (initializerContext: PluginInitializerContext) =>
new CasePlugin(initializerContext);
export interface PluginStartContract {
getCasesClientWithRequestAndContext(
context: RequestHandlerContext,
request: KibanaRequest
): CasesClient;
}

View file

@ -5,7 +5,13 @@
* 2.0.
*/
import { IContextProvider, KibanaRequest, Logger, PluginInitializerContext } from 'kibana/server';
import {
IContextProvider,
KibanaRequest,
Logger,
PluginInitializerContext,
RequestHandlerContext,
} from 'kibana/server';
import { CoreSetup, CoreStart } from 'src/core/server';
import { SecurityPluginSetup } from '../../security/server';
@ -128,7 +134,7 @@ export class CasePlugin {
this.log.debug(`Starting Case Workflow`);
const getCasesClientWithRequestAndContext = async (
context: CasesRequestHandlerContext,
context: RequestHandlerContext,
request: KibanaRequest
) => {
const user = await this.caseService!.getUser({ request });

View file

@ -12,6 +12,10 @@ import {
SavedObjectsClientContract,
} from 'src/core/server';
import { ExceptionListClient } from '../../../lists/server';
import {
CasesClient,
PluginStartContract as CasesPluginStartContract,
} from '../../../cases/server';
import { SecurityPluginStart } from '../../../security/server';
import {
AgentService,
@ -41,6 +45,7 @@ import {
ExperimentalFeatures,
parseExperimentalConfigValue,
} from '../../common/experimental_features';
import { SecuritySolutionRequestHandlerContext } from '../types';
export interface MetadataService {
queryStrategy(
@ -98,6 +103,7 @@ export type EndpointAppContextServiceStartContract = Partial<
savedObjectsStart: SavedObjectsServiceStart;
licenseService: LicenseService;
exceptionListsClient: ExceptionListClient | undefined;
cases: CasesPluginStartContract | undefined;
};
/**
@ -114,6 +120,7 @@ export class EndpointAppContextService {
private config: ConfigType | undefined;
private license: LicenseService | undefined;
public security: SecurityPluginStart | undefined;
private cases: CasesPluginStartContract | undefined;
private experimentalFeatures: ExperimentalFeatures | undefined;
@ -127,6 +134,7 @@ export class EndpointAppContextService {
this.config = dependencies.config;
this.license = dependencies.licenseService;
this.security = dependencies.security;
this.cases = dependencies.cases;
this.experimentalFeatures = parseExperimentalConfigValue(this.config.enableExperimental);
@ -191,4 +199,14 @@ export class EndpointAppContextService {
}
return this.license;
}
public async getCasesClient(
req: KibanaRequest,
context: SecuritySolutionRequestHandlerContext
): Promise<CasesClient> {
if (!this.cases) {
throw new Error(`must call start on ${EndpointAppContextService.name} to call getter`);
}
return this.cases.getCasesClientWithRequestAndContext(context, req);
}
}

View file

@ -87,6 +87,9 @@ export const createMockEndpointAppContextServiceStartContract = (): jest.Mocked<
>(),
exceptionListsClient: listMock.getExceptionListClient(),
packagePolicyService: createPackagePolicyServiceMock(),
cases: {
getCasesClientWithRequestAndContext: jest.fn(),
},
};
};

View file

@ -9,6 +9,7 @@ import moment from 'moment';
import { RequestHandler } from 'src/core/server';
import uuid from 'uuid';
import { TypeOf } from '@kbn/config-schema';
import { CommentType } from '../../../../../cases/common';
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';
@ -104,6 +105,20 @@ export const isolationRequestHandler = function (
}
agentIDs = [...new Set(agentIDs)]; // dedupe
// convert any alert IDs into cases
let caseIDs: string[] = req.body.case_ids?.slice() || [];
if (req.body.alert_ids && req.body.alert_ids.length > 0) {
const newIDs: string[][] = await Promise.all(
req.body.alert_ids.map(async (a: string) =>
(await endpointContext.service.getCasesClient(req, context)).getCaseIdsByAlertId({
alertId: a,
})
)
);
caseIDs = caseIDs.concat(...newIDs);
}
caseIDs = [...new Set(caseIDs)];
// create an Action ID and dispatch it to ES & Fleet Server
const esClient = context.core.elasticsearch.client.asCurrentUser;
const actionID = uuid.v4();
@ -140,6 +155,29 @@ export const isolationRequestHandler = function (
},
});
}
const commentLines: string[] = [];
commentLines.push(`${isolate ? 'I' : 'Uni'}solate action was sent to the following Agents:`);
// lines of markdown links, inside a code block
commentLines.push(
`${agentIDs.map((a) => `- [${a}](/app/fleet#/fleet/agents/${a})`).join('\n')}`
);
if (req.body.comment) {
commentLines.push(`\n\nWith Comment:\n> ${req.body.comment}`);
}
caseIDs.forEach(async (caseId) => {
(await endpointContext.service.getCasesClient(req, context)).addComment({
caseId,
comment: {
comment: commentLines.join('\n'),
type: CommentType.user,
},
});
});
return res.ok({
body: {
action: actionID,

View file

@ -27,6 +27,7 @@ import {
PluginSetupContract as AlertingSetup,
PluginStartContract as AlertPluginStartContract,
} from '../../alerting/server';
import { PluginStartContract as CasesPluginStartContract } from '../../cases/server';
import { SecurityPluginSetup as SecuritySetup, SecurityPluginStart } from '../../security/server';
import { PluginSetupContract as FeaturesSetup } from '../../features/server';
import { MlPluginSetup as MlSetup } from '../../ml/server';
@ -101,6 +102,7 @@ export interface StartPlugins {
taskManager?: TaskManagerStartContract;
telemetry?: TelemetryPluginStart;
security: SecurityPluginStart;
cases?: CasesPluginStartContract;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
@ -402,6 +404,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
security: plugins.security,
alerting: plugins.alerting,
config: this.config!,
cases: plugins.cases,
logger,
manifestManager,
registerIngestCallback,