[Security Solution] Interim Host Isolation Case Commenting (#100092)
This commit is contained in:
parent
25cad22b3d
commit
97cc6ddb6b
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ export const createExternalCasesClientMock = (): CasesClientPluginContractMock =
|
|||
getUserActions: jest.fn(),
|
||||
update: jest.fn(),
|
||||
updateAlertsStatus: jest.fn(),
|
||||
getCaseIdsByAlertId: jest.fn(),
|
||||
});
|
||||
|
||||
export const createCasesClientWithMockSavedObjectsClient = async ({
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,6 +87,9 @@ export const createMockEndpointAppContextServiceStartContract = (): jest.Mocked<
|
|||
>(),
|
||||
exceptionListsClient: listMock.getExceptionListClient(),
|
||||
packagePolicyService: createPackagePolicyServiceMock(),
|
||||
cases: {
|
||||
getCasesClientWithRequestAndContext: jest.fn(),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue