Skip to content

Commit

Permalink
Refactoring update alert status for sub cases and removing request an…
Browse files Browse the repository at this point in the history
…d cleaning up
  • Loading branch information
jonathan-buttner committed Feb 11, 2021
1 parent f78ba23 commit b6c296e
Show file tree
Hide file tree
Showing 34 changed files with 717 additions and 325 deletions.
65 changes: 51 additions & 14 deletions x-pack/plugins/case/common/api/cases/comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export const CommentAttributesBasicRt = rt.type({
updated_by: rt.union([UserRT, rt.null]),
});

export const GeneratedAlertRequestTypeField = 'generated_alert_request';

export enum CommentType {
user = 'user',
alert = 'alert',
Expand All @@ -44,34 +46,50 @@ export const ContextTypeUserRt = rt.type({
type: rt.literal(CommentType.user),
});

export const ContextTypeAlertRt = rt.type({
/**
* This defines the structure of how alerts (generated or user attached) are stored in saved objects documents.
*/
export const AlertCommentAttributesRt = rt.type({
type: rt.union([rt.literal(CommentType.generatedAlert), rt.literal(CommentType.alert)]),
alertId: rt.union([rt.array(rt.string), rt.string]),
index: rt.string,
});

/**
* This defines the structure of an alert attached by a user.
*/
export const AlertCommentRequestRt = rt.type({
type: rt.literal(CommentType.alert),
alertId: rt.union([rt.array(rt.string), rt.string]),
index: rt.string,
});

const AlertIDRt = rt.type({
_id: rt.string,
});

export const ContextTypeGeneratedAlertRt = rt.type({
type: rt.literal(CommentType.generatedAlert),
/**
* This defines the structure of generated alerts attached by a detections rule.
*/
export const GeneratedAlertCommentRequestRt = rt.type({
type: rt.literal(GeneratedAlertRequestTypeField),
alerts: rt.union([rt.array(AlertIDRt), AlertIDRt]),
index: rt.string,
});

const AttributesTypeUserRt = rt.intersection([ContextTypeUserRt, CommentAttributesBasicRt]);
const AttributesTypeAlertsRt = rt.intersection([ContextTypeAlertRt, CommentAttributesBasicRt]);
const AttributesTypeAlertsRt = rt.intersection([
AlertCommentAttributesRt,
CommentAttributesBasicRt,
]);
const CommentAttributesRt = rt.union([AttributesTypeUserRt, AttributesTypeAlertsRt]);

const ContextBasicRt = rt.union([
export const CommentRequestRt = rt.union([
ContextTypeUserRt,
ContextTypeAlertRt,
ContextTypeGeneratedAlertRt,
AlertCommentRequestRt,
GeneratedAlertCommentRequestRt,
]);

export const CommentRequestRt = ContextBasicRt;

export const CommentResponseRt = rt.intersection([
CommentAttributesRt,
rt.type({
Expand All @@ -90,12 +108,14 @@ export const CommentResponseTypeAlertsRt = rt.intersection([

export const AllCommentsResponseRT = rt.array(CommentResponseRt);

const CommentPatchRequestTypesRt = rt.union([ContextTypeUserRt, AlertCommentAttributesRt]);

export const CommentPatchRequestRt = rt.intersection([
/**
* Partial updates are not allowed.
* We want to prevent the user for changing the type without removing invalid fields.
*/
rt.union([ContextTypeUserRt, ContextTypeAlertRt]),
CommentPatchRequestTypesRt,
rt.type({ id: rt.string, version: rt.string }),
]);

Expand All @@ -106,7 +126,10 @@ export const CommentPatchRequestRt = rt.intersection([
* We ensure that partial updates of CommentContext is not going to happen inside the patch comment route.
*/
export const CommentPatchAttributesRt = rt.intersection([
rt.union([rt.partial(CommentAttributesBasicRt.props), rt.partial(ContextTypeAlertRt.props)]),
rt.union([
rt.partial(CommentAttributesBasicRt.props),
rt.partial(AlertCommentAttributesRt.props),
]),
rt.partial(CommentAttributesBasicRt.props),
]);

Expand All @@ -119,7 +142,6 @@ export const CommentsResponseRt = rt.type({

export const AllCommentsResponseRt = rt.array(CommentResponseRt);

export type AttributesTypeAlerts = rt.TypeOf<typeof AttributesTypeAlertsRt>;
export type CommentAttributes = rt.TypeOf<typeof CommentAttributesRt>;
export type CommentRequest = rt.TypeOf<typeof CommentRequestRt>;
export type CommentResponse = rt.TypeOf<typeof CommentResponseRt>;
Expand All @@ -129,11 +151,26 @@ export type CommentsResponse = rt.TypeOf<typeof CommentsResponseRt>;
export type CommentPatchRequest = rt.TypeOf<typeof CommentPatchRequestRt>;
export type CommentPatchAttributes = rt.TypeOf<typeof CommentPatchAttributesRt>;
export type CommentRequestUserType = rt.TypeOf<typeof ContextTypeUserRt>;
export type CommentRequestAlertType = rt.TypeOf<typeof ContextTypeAlertRt>;
export type CommentRequestAlertType = rt.TypeOf<typeof AlertCommentRequestRt>;

/**
* This is different than a CommentRequest because a patch request only allows the alert and user types to be patched.
* A CommentRequest allows alerts, user, and generated_alerts
*/
export type CommentPatchRequestTypes = rt.TypeOf<typeof CommentPatchRequestTypesRt>;

/**
* This type includes the index, alertIds, and the basic attributes (who added it, when etc)
*/
export type AttributesTypeAlerts = rt.TypeOf<typeof AttributesTypeAlertsRt>;
/**
* This type includes only the index, alertIds, it does not include the basic attributes.
*/
export type AttributesTypeAlertsWithoutBasic = rt.TypeOf<typeof AlertCommentAttributesRt>;

/**
* This type represents a generated alert (or alerts) from a rule. The difference between this and a user alert
* is that this format expects the included alerts to have the structure { _id: string }. When it is saved the outer
* object will be stripped off and the _id will be stored in the alertId field.
*/
export type CommentRequestGeneratedAlertType = rt.TypeOf<typeof ContextTypeGeneratedAlertRt>;
export type CommentRequestGeneratedAlertType = rt.TypeOf<typeof GeneratedAlertCommentRequestRt>;
1 change: 0 additions & 1 deletion x-pack/plugins/case/server/client/alerts/update_status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ interface UpdateAlertsStatusArgs {
scopedClusterClient: ElasticsearchClient;
}

// TODO: remove this file
export const updateAlertsStatus = async ({
alertsService,
ids,
Expand Down
9 changes: 5 additions & 4 deletions x-pack/plugins/case/server/client/cases/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';

import { KibanaRequest, SavedObjectsClientContract } from 'src/core/server';
import { SavedObjectsClientContract } from 'src/core/server';
import { flattenCaseSavedObject, transformNewCase } from '../../routes/api/utils';

import {
Expand All @@ -21,6 +21,7 @@ import {
CaseClientPostRequestRt,
CasePostRequest,
CaseType,
User,
} from '../../../common/api';
import { buildCaseUserActionItem } from '../../services/user_actions/helpers';
import {
Expand All @@ -37,7 +38,7 @@ import {
interface CreateCaseArgs {
caseConfigureService: CaseConfigureServiceSetup;
caseService: CaseServiceSetup;
request: KibanaRequest;
user: User;
savedObjectsClient: SavedObjectsClientContract;
userActionService: CaseUserActionServiceSetup;
theCase: CasePostRequest;
Expand All @@ -48,7 +49,7 @@ export const create = async ({
caseService,
caseConfigureService,
userActionService,
request,
user,
theCase,
}: CreateCaseArgs): Promise<CaseResponse> => {
// default to an individual case if the type is not defined.
Expand All @@ -60,7 +61,7 @@ export const create = async ({
);

// eslint-disable-next-line @typescript-eslint/naming-convention
const { username, full_name, email } = await caseService.getUser({ request });
const { username, full_name, email } = user;
const createdDate = new Date().toISOString();
const myCaseConfigure = await caseConfigureService.find({ client: savedObjectsClient });
const caseConfigureConnector = getConnectorFromConfiguration(myCaseConfigure);
Expand Down
43 changes: 14 additions & 29 deletions x-pack/plugins/case/server/client/cases/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@

import Boom, { isBoom, Boom as BoomType } from '@hapi/boom';
import {
KibanaRequest,
SavedObjectsBulkUpdateResponse,
SavedObjectsClientContract,
SavedObjectsUpdateResponse,
} from 'kibana/server';
import { ActionsClient } from '../../../../actions/server';
import { flattenCaseSavedObject, getAlertIds } from '../../routes/api/utils';
import { ActionResult, ActionsClient } from '../../../../actions/server';
import { flattenCaseSavedObject, getAlertIndicesAndIDs } from '../../routes/api/utils';

import {
ActionConnector,
Expand All @@ -23,16 +22,18 @@ import {
ExternalServiceResponse,
ESCaseAttributes,
CommentAttributes,
CaseUserActionsResponse,
User,
} from '../../../common/api';
import { buildCaseUserActionItem } from '../../services/user_actions/helpers';

import { createIncident, getCommentContextFromAttributes, isCommentAlertType } from './utils';
import { createIncident, getCommentContextFromAttributes } from './utils';
import {
CaseConfigureServiceSetup,
CaseServiceSetup,
CaseUserActionServiceSetup,
} from '../../services';
import { CaseClientImpl } from '../client';
import { CaseClientHandler } from '../client';

const createError = (e: Error | BoomType, message: string): Error | BoomType => {
if (isBoom(e)) {
Expand All @@ -44,20 +45,15 @@ const createError = (e: Error | BoomType, message: string): Error | BoomType =>
return Error(message);
};

interface AlertInfo {
ids: string[];
indices: Set<string>;
}

interface PushParams {
savedObjectsClient: SavedObjectsClientContract;
caseService: CaseServiceSetup;
caseConfigureService: CaseConfigureServiceSetup;
userActionService: CaseUserActionServiceSetup;
request: KibanaRequest;
user: User;
caseId: string;
connectorId: string;
caseClient: CaseClientImpl;
caseClient: CaseClientHandler;
actionsClient: ActionsClient;
}

Expand All @@ -66,16 +62,16 @@ export const push = async ({
caseService,
caseConfigureService,
userActionService,
request,
caseClient,
actionsClient,
connectorId,
caseId,
user,
}: PushParams): Promise<CaseResponse> => {
/* Start of push to external service */
let theCase;
let connector;
let userActions;
let theCase: CaseResponse;
let connector: ActionResult;
let userActions: CaseUserActionsResponse;
let alerts;
let connectorMappings;
let externalServiceIncident;
Expand All @@ -98,16 +94,7 @@ export const push = async ({
);
}

const { ids, indices }: AlertInfo = theCase?.comments
?.filter(isCommentAlertType)
.reduce<AlertInfo>(
(acc, comment) => {
acc.ids.push(...getAlertIds(comment));
acc.indices.add(comment.index);
return acc;
},
{ ids: [], indices: new Set<string>() }
) ?? { ids: [], indices: new Set<string>() };
const { ids, indices } = getAlertIndicesAndIDs(theCase?.comments);

try {
alerts = await caseClient.getAlerts({
Expand Down Expand Up @@ -160,14 +147,12 @@ export const push = async ({
/* End of push to external service */

/* Start of update case with push information */
let user;
let myCase;
let myCaseConfigure;
let comments;

try {
[user, myCase, myCaseConfigure, comments] = await Promise.all([
caseService.getUser({ request }),
[myCase, myCaseConfigure, comments] = await Promise.all([
caseService.getCase({
client: savedObjectsClient,
id: caseId,
Expand Down
51 changes: 44 additions & 7 deletions x-pack/plugins/case/server/client/cases/update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,18 @@ describe('update', () => {

const savedObjectsClient = createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: [{ ...mockCaseComments[3] }],
caseCommentSavedObject: [
{
...mockCaseComments[3],
references: [
{
type: 'cases',
name: 'associated-cases',
id: 'mock-id-1',
},
],
},
],
});

const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
Expand Down Expand Up @@ -531,24 +542,50 @@ describe('update', () => {
...mockCases[1],
},
],
caseCommentSavedObject: [{ ...mockCaseComments[3] }, { ...mockCaseComments[4] }],
caseCommentSavedObject: [
{
...mockCaseComments[3],
references: [
{
type: 'cases',
name: 'associated-cases',
id: 'mock-id-1',
},
],
},
{
...mockCaseComments[4],
references: [
{
type: 'cases',
name: 'associated-cases',
id: 'mock-id-2',
},
],
},
],
});

const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
caseClient.client.updateAlertsStatus = jest.fn();

await caseClient.client.update(patchCases);

/**
* the update code will put each comment into a status bucket and then make at most 1 call
* to ES for each status bucket
* Now instead of doing a call per case to get the comments, it will do a single call with all the cases
* and sub cases and get all the comments in one go
*/
expect(caseClient.client.updateAlertsStatus).toHaveBeenNthCalledWith(1, {
ids: ['test-id', 'test-id-2'],
ids: ['test-id'],
status: 'open',
indices: new Set<string>(['test-index', 'test-index-2']),
indices: new Set<string>(['test-index']),
});

expect(caseClient.client.updateAlertsStatus).toHaveBeenNthCalledWith(2, {
ids: ['test-id', 'test-id-2'],
ids: ['test-id-2'],
status: 'closed',
indices: new Set<string>(['test-index', 'test-index-2']),
indices: new Set<string>(['test-index-2']),
});
});

Expand Down
Loading

0 comments on commit b6c296e

Please sign in to comment.