Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Cases] Update cases ids in the alerts schema when attaching an alert to a case #147985

Merged
merged 26 commits into from
Feb 11, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5f5ef30
Extend alerts client to update case ids
cnasikas Dec 22, 2022
799d296
Pass the alertsClient to the cases client
cnasikas Dec 14, 2022
235c5e5
Update alerts schema when attaching alerts to a case
cnasikas Dec 22, 2022
c466e46
Check total cases limit per alert
cnasikas Dec 23, 2022
9796e97
Add integration checks
cnasikas Dec 23, 2022
340b86f
Fix types
cnasikas Dec 23, 2022
f9205c0
Add o11y tests
cnasikas Jan 11, 2023
3ffe519
Add rbac tests
cnasikas Jan 11, 2023
f99aad0
Address TODOs
cnasikas Jan 12, 2023
994be50
Integration tests: check for uniqueness
cnasikas Jan 12, 2023
023ea28
PR feedback
cnasikas Jan 13, 2023
dc90b04
Merge branch 'main' into link_alerts_to_case
cnasikas Jan 13, 2023
4b12ee4
Merge branch 'main' into link_alerts_to_case
cnasikas Jan 16, 2023
00f880c
Change schema to do one mget call
cnasikas Jan 16, 2023
82c69bc
Move updating case ids to the alerts service
cnasikas Jan 16, 2023
5f48d8d
Filter out empty alets
cnasikas Jan 16, 2023
bd6c48d
Do not call the alerts client if there are no alerts to update
cnasikas Jan 17, 2023
acd4186
Merge branch 'main' into link_alerts_to_case
cnasikas Feb 7, 2023
8419345
Add ensureAllAlertsAuthorizedRead to the alerts client
cnasikas Feb 7, 2023
aafdf84
Authorize alerts when attached to a case
cnasikas Feb 8, 2023
a9e5562
More tests
cnasikas Feb 8, 2023
6e1f3cf
Merge branch 'main' into link_alerts_to_case
cnasikas Feb 8, 2023
b41bd88
Merge branch 'main' into link_alerts_to_case
cnasikas Feb 9, 2023
7e5e8c0
Fix conflicts
cnasikas Feb 9, 2023
ae05f46
PR feedback
cnasikas Feb 10, 2023
5e11b24
Merge branch 'main' into link_alerts_to_case
cnasikas Feb 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/kbn-rule-data-utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export * from './src/technical_field_names';
export * from './src/alerts_as_data_rbac';
export * from './src/alerts_as_data_severity';
export * from './src/alerts_as_data_status';
export * from './src/alerts_as_data_cases';
export * from './src/routes/stack_rule_paths';
9 changes: 9 additions & 0 deletions packages/kbn-rule-data-utils/src/alerts_as_data_cases.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

export const MAX_CASES_PER_ALERT = 10;
3 changes: 2 additions & 1 deletion x-pack/plugins/cases/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
"management",
"spaces",
"security",
"notifications"
"notifications",
"ruleRegistry"
],
"requiredBundles": [
"savedObjects"
Expand Down
4 changes: 2 additions & 2 deletions x-pack/plugins/cases/server/client/alerts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ export type CasesClientGetAlertsResponse = Alert[];
/**
* Defines the fields necessary to update an alert's status.
*/
export interface UpdateAlertRequest {
export interface UpdateAlertStatusRequest {
id: string;
index: string;
status: CaseStatuses;
}

export interface AlertUpdateStatus {
alerts: UpdateAlertRequest[];
alerts: UpdateAlertStatusRequest[];
}

export interface AlertGet {
Expand Down
4 changes: 2 additions & 2 deletions x-pack/plugins/cases/server/client/cases/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { CASE_COMMENT_SAVED_OBJECT } from '../../../common/constants';
import { createIncident, getDurationInSeconds } from './utils';
import { createCaseError } from '../../common/error';
import {
createAlertUpdateRequest,
createAlertUpdateStatusRequest,
flattenCaseSavedObject,
getAlertInfoFromComments,
} from '../../common/utils';
Expand Down Expand Up @@ -68,7 +68,7 @@ const changeAlertsStatusToClose = async (

const alerts = alertAttachments.saved_objects
.map((attachment) =>
createAlertUpdateRequest({
createAlertUpdateStatusRequest({
comment: attachment.attributes,
status: CaseStatuses.closed,
})
Expand Down
8 changes: 4 additions & 4 deletions x-pack/plugins/cases/server/client/cases/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@ import { arraysDifference, getCaseToUpdate } from '../utils';
import type { AlertService, CasesService } from '../../services';
import { createCaseError } from '../../common/error';
import {
createAlertUpdateRequest,
createAlertUpdateStatusRequest,
flattenCaseSavedObject,
isCommentRequestTypeAlert,
} from '../../common/utils';
import type { UpdateAlertRequest } from '../alerts/types';
import type { UpdateAlertStatusRequest } from '../alerts/types';
import type { CasesClientArgs } from '..';
import type { OwnerEntity } from '../../authorization';
import { Operations } from '../../authorization';
Expand Down Expand Up @@ -238,14 +238,14 @@ async function updateAlerts({

// create an array of requests that indicate the id, index, and status to update an alert
const alertsToUpdate = totalAlerts.saved_objects.reduce(
(acc: UpdateAlertRequest[], alertComment) => {
(acc: UpdateAlertStatusRequest[], alertComment) => {
if (isCommentRequestTypeAlert(alertComment.attributes)) {
const status = getSyncStatusForComment({
alertComment,
casesToSyncToStatus,
});

acc.push(...createAlertUpdateRequest({ comment: alertComment.attributes, status }));
acc.push(...createAlertUpdateStatusRequest({ comment: alertComment.attributes, status }));
}

return acc;
Expand Down
7 changes: 6 additions & 1 deletion x-pack/plugins/cases/server/client/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import type { LensServerPluginSetup } from '@kbn/lens-plugin/server';
import type { SpacesPluginStart } from '@kbn/spaces-plugin/server';
import type { LicensingPluginStart } from '@kbn/licensing-plugin/server';
import type { NotificationsPluginStart } from '@kbn/notifications-plugin/server';
import type { RuleRegistryPluginStartContract } from '@kbn/rule-registry-plugin/server';

import { SAVED_OBJECT_TYPES } from '../../common/constants';
import { Authorization } from '../authorization/authorization';
import {
Expand Down Expand Up @@ -53,10 +55,11 @@ interface CasesClientFactoryArgs {
actionsPluginStart: ActionsPluginStart;
licensingPluginStart: LicensingPluginStart;
lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory'];
notifications: NotificationsPluginStart;
ruleRegistry: RuleRegistryPluginStartContract;
persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry;
externalReferenceAttachmentTypeRegistry: ExternalReferenceAttachmentTypeRegistry;
publicBaseUrl?: IBasePath['publicBaseUrl'];
notifications: NotificationsPluginStart;
}

/**
Expand Down Expand Up @@ -127,6 +130,7 @@ export class CasesClientFactory {
});

const userInfo = await this.getUserInfo(request);
const alertsClient = await this.options.ruleRegistry.getRacClientWithRequest(request);

return createCasesClient({
services,
Expand All @@ -141,6 +145,7 @@ export class CasesClientFactory {
securityStartPlugin: this.options.securityPluginStart,
publicBaseUrl: this.options.publicBaseUrl,
spaceId: this.options.spacesPluginStart.spacesService.getSpaceId(request),
alertsClient,
});
}

Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/cases/server/client/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { loggingSystemMock, savedObjectsClientMock } from '@kbn/core/server/mock
import { securityMock } from '@kbn/security-plugin/server/mocks';

import { actionsClientMock } from '@kbn/actions-plugin/server/actions_client.mock';
import { alertsClientMock } from '@kbn/rule-registry-plugin/server/alert_data_client/alerts_client.mock';
import { makeLensEmbeddableFactory } from '@kbn/lens-plugin/server/embeddable/make_lens_embeddable_factory';
import type { CasesClient } from '.';
import { createAuthorizationMock } from '../authorization/mock';
Expand Down Expand Up @@ -140,6 +141,7 @@ export const createCasesClientMockArgs = () => {
logger: loggingSystemMock.createLogger(),
unsecuredSavedObjectsClient: savedObjectsClientMock.create(),
actionsClient: actionsClientMock.create(),
alertsClient: alertsClientMock.create(),
user: {
username: 'damaged_raccoon',
email: 'damaged_raccoon@elastic.co',
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/cases/server/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { LensServerPluginSetup } from '@kbn/lens-plugin/server';
import type { SecurityPluginStart } from '@kbn/security-plugin/server';
import type { IBasePath } from '@kbn/core-http-browser';
import type { KueryNode } from '@kbn/es-query';
import type { AlertsClient } from '@kbn/rule-registry-plugin/server';
import type { CasesFindRequest, User } from '../../common/api';
import type { Authorization } from '../authorization/authorization';
import type {
Expand Down Expand Up @@ -53,6 +54,7 @@ export interface CasesClientArgs {
readonly externalReferenceAttachmentTypeRegistry: ExternalReferenceAttachmentTypeRegistry;
readonly securityStartPlugin: SecurityPluginStart;
readonly spaceId: string;
readonly alertsClient: PublicMethodsOf<AlertsClient>;
readonly publicBaseUrl?: IBasePath['publicBaseUrl'];
}

Expand Down
51 changes: 45 additions & 6 deletions x-pack/plugins/cases/server/common/models/case_with_comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
SavedObjectsUpdateOptions,
SavedObjectsUpdateResponse,
} from '@kbn/core/server';
import pMap from 'p-map';
import type {
CaseResponse,
CommentAttributes,
Expand All @@ -30,6 +31,7 @@ import {
import {
CASE_SAVED_OBJECT,
MAX_ALERTS_PER_CASE,
MAX_CONCURRENT_SEARCHES,
MAX_DOCS_PER_PAGE,
} from '../../../common/constants';
import type { CasesClientArgs } from '../../client';
Expand All @@ -41,11 +43,13 @@ import {
flattenCommentSavedObjects,
transformNewComment,
getOrUpdateLensReferences,
createAlertUpdateRequest,
createAlertUpdateStatusRequest,
isCommentRequestTypeAlert,
getAlertInfoFromComments,
} from '../utils';

type CaseCommentModelParams = Omit<CasesClientArgs, 'authorization'>;

const ALERT_LIMIT_MSG = `Case has reached the maximum allowed number (${MAX_ALERTS_PER_CASE}) of attached alerts.`;

/**
Expand Down Expand Up @@ -310,18 +314,21 @@ export class CaseCommentModel {
}

private async handleAlertComments(attachments: CommentRequest[]) {
const alerts = attachments.filter(
(attachment) =>
attachment.type === CommentType.alert && this.caseInfo.attributes.settings.syncAlerts
const alertAttachments = attachments.filter(
(attachment): attachment is CommentRequestAlertType => attachment.type === CommentType.alert
);

await this.updateAlertsStatus(alerts);
if (this.caseInfo.attributes.settings.syncAlerts) {
await this.updateAlertsStatus(alertAttachments);
}

await this.updateAlertsSchemaWithCaseInfo(alertAttachments);
}

private async updateAlertsStatus(alerts: CommentRequest[]) {
const alertsToUpdate = alerts
.map((alert) =>
createAlertUpdateRequest({
createAlertUpdateStatusRequest({
comment: alert,
status: this.caseInfo.attributes.status,
})
Expand All @@ -331,6 +338,38 @@ export class CaseCommentModel {
await this.params.services.alertsService.updateAlertsStatus(alertsToUpdate);
}

private async updateAlertsSchemaWithCaseInfo(alertAttachments: CommentRequestAlertType[]) {
cnasikas marked this conversation as resolved.
Show resolved Hide resolved
try {
const alerts = getAlertInfoFromComments(alertAttachments);
cnasikas marked this conversation as resolved.
Show resolved Hide resolved
const alertsGroupedByIndex = new Map<string, Set<string>>();

for (const alert of alerts) {
const idsSet = alertsGroupedByIndex.get(alert.index) ?? new Set();
idsSet.add(alert.id);
alertsGroupedByIndex.set(alert.index, idsSet);
}

await pMap(
cnasikas marked this conversation as resolved.
Show resolved Hide resolved
alertsGroupedByIndex.entries(),
async ([index, idsSet]) =>
this.params.alertsClient.bulkUpdateCases({
ids: Array.from(idsSet.values()),
index,
caseIds: [this.caseInfo.id],
}),
{
concurrency: MAX_CONCURRENT_SEARCHES,
}
);
} catch (error) {
throw createCaseError({
message: `Failed to add case info to alerts for caseId ${this.caseInfo.id}: ${error}`,
error,
logger: this.params.logger,
});
}
}

private async createCommentUserAction(
comment: SavedObject<CommentAttributes>,
req: CommentRequest
Expand Down
6 changes: 3 additions & 3 deletions x-pack/plugins/cases/server/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import {
ConnectorTypes,
ExternalReferenceStorageType,
} from '../../common/api';
import type { UpdateAlertRequest } from '../client/alerts/types';
import type { UpdateAlertStatusRequest } from '../client/alerts/types';
import {
parseCommentString,
getLensVisualizations,
Expand Down Expand Up @@ -267,13 +267,13 @@ export const isCommentRequestTypeExternalReferenceSO = (
/**
* Adds the ids and indices to a map of statuses
*/
export function createAlertUpdateRequest({
export function createAlertUpdateStatusRequest({
comment,
status,
}: {
comment: CommentRequest;
status: CaseStatuses;
}): UpdateAlertRequest[] {
}): UpdateAlertStatusRequest[] {
return getAlertInfoFromComments([comment]).map((alert) => ({ ...alert, status }));
}

Expand Down
3 changes: 3 additions & 0 deletions x-pack/plugins/cases/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type {
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
import type { LicensingPluginSetup, LicensingPluginStart } from '@kbn/licensing-plugin/server';
import type { NotificationsPluginStart } from '@kbn/notifications-plugin/server';
import type { RuleRegistryPluginStartContract } from '@kbn/rule-registry-plugin/server';

import { APP_ID } from '../common/constants';
import {
Expand Down Expand Up @@ -74,6 +75,7 @@ export interface PluginsStart {
security: SecurityPluginStart;
spaces: SpacesPluginStart;
notifications: NotificationsPluginStart;
ruleRegistry: RuleRegistryPluginStartContract;
}

export class CasePlugin {
Expand Down Expand Up @@ -202,6 +204,7 @@ export class CasePlugin {
externalReferenceAttachmentTypeRegistry: this.externalReferenceAttachmentTypeRegistry,
publicBaseUrl: core.http.basePath.publicBaseUrl,
notifications: plugins.notifications,
ruleRegistry: plugins.ruleRegistry,
});

const client = core.elasticsearch.client;
Expand Down
8 changes: 4 additions & 4 deletions x-pack/plugins/cases/server/services/alerts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { CaseStatuses } from '../../../common/api';
import { MAX_ALERTS_PER_CASE, MAX_CONCURRENT_SEARCHES } from '../../../common/constants';
import { createCaseError } from '../../common/error';
import type { AlertInfo } from '../../common/types';
import type { UpdateAlertRequest } from '../../client/alerts/types';
import type { UpdateAlertStatusRequest } from '../../client/alerts/types';
import type { AggregationBuilder, AggregationResponse } from '../../client/metrics/types';

export class AlertService {
Expand Down Expand Up @@ -75,7 +75,7 @@ export class AlertService {
};
}

public async updateAlertsStatus(alerts: UpdateAlertRequest[]) {
public async updateAlertsStatus(alerts: UpdateAlertStatusRequest[]) {
try {
const bucketedAlerts = this.bucketAlertsByIndexAndStatus(alerts);
const indexBuckets = Array.from(bucketedAlerts.entries());
Expand All @@ -96,7 +96,7 @@ export class AlertService {
}

private bucketAlertsByIndexAndStatus(
alerts: UpdateAlertRequest[]
alerts: UpdateAlertStatusRequest[]
): Map<string, Map<STATUS_VALUES, TranslatedUpdateAlertRequest[]>> {
return alerts.reduce<Map<string, Map<STATUS_VALUES, TranslatedUpdateAlertRequest[]>>>(
(acc, alert) => {
Expand Down Expand Up @@ -130,7 +130,7 @@ export class AlertService {
return isEmpty(alert.id) || isEmpty(alert.index);
}

private translateStatus(alert: UpdateAlertRequest): STATUS_VALUES {
private translateStatus(alert: UpdateAlertStatusRequest): STATUS_VALUES {
const translatedStatuses: Record<string, STATUS_VALUES> = {
[CaseStatuses.open]: 'open',
[CaseStatuses['in-progress']]: 'acknowledged',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const createAlertsClientMock = () => {
update: jest.fn(),
getAuthorizedAlertsIndices: jest.fn(),
bulkUpdate: jest.fn(),
bulkUpdateCases: jest.fn(),
find: jest.fn(),
getFeatureIdsByRegistrationContexts: jest.fn(),
getBrowserFields: jest.fn(),
Expand Down
Loading