From 97d391a6360f029b03af255ef605d895ffaa8863 Mon Sep 17 00:00:00 2001
From: Angela Chuang <6295984+angorayc@users.noreply.github.com>
Date: Thu, 18 Feb 2021 01:32:40 +0000
Subject: [PATCH] [Security Solution] Detection rules for case UI (#91434)
* Adding type field to client
* Removing context and adding association type
* Handle alerts from multiple indices
* Adding flow for adding a sub case
* Making progress on creating alerts from rules
* Refactored add comment to handle case and sub case
* Starting sub case API and refactoring of case client
* Fleshing out find cases
* Finished the find cases api
* Filtering comments by association type
* Fixing tests and types
* Updating snapshots
* Cleaning up comment references
* Working unit tests
* Fixing integration tests and got ES to work
* Unit tests and api integration test working
* Refactoring find and get_status
* Starting patch, and update
* script for sub cases
* Removing converted_by and fixing type errors
* Adding docs for script
* Removing converted_by and fixing integration test
* init expanded rows
* Adding sub case id to comment routes
* Removing stringify comparison
* styling
* clean up
* add status column
* styling
* hide actions if it has sub-cases
* Adding delete api and tests
* generated alert
* Updating license
* missed license files
* Integration tests passing
* Adding more tests for sub cases
* wip
* Find int tests, scoped client, patch sub user actions
* fixing types and call cluster
* fixing get sub case param issue
* Adding user actions for sub cases
* Preventing alerts on collections and refactoring user
* Allowing type to be updated for ind cases
* subcases attached to api
* combine enum on UI for simplification
* Refactoring and writing tests
* Fixing sub case status filtering
* add alerts count
* Adding more tests not allowing gen alerts patch
* Working unit tests
* Push to connector gets all sub case comments
* Writing more tests and cleaning up
* Updating push functionality for generated alerts and sub cases
* Adding comment about updating collection sync
* use CaseType to check if it is a sub-case
* fix types and disable selection if it has subcases
* isEmpty
* Detection rule correctly adding alerts to sub case
* update api and functionality to accept sub case
* integration part I
* fix integration with case connector
* Fix manual attach
* Fix types
* Fix bug when updating
* Fix bug with user actions
* Fix react key error
* Fix bug when pushing a lot of alerts
* fix lint error
* Fix limit
* fix title on sub case
* fix unit tests
* rm bazel
* fix unit tests and cypress test
* enable delete case icon
* revert change
* review
* Fix the scripts alerts generation code
* temp work
* Fix rule types and add migration
* fix types
* fix types error
* Remove query alerts
* Fix rules
* fix types
* fix lint error
* fix types
* delete a sub case
* rm unused i18n
* fix delete cases
* fix unit tests
* fix unit test
* update Case type
* fix types
* fix unit test
* final integration between rule and case
* fix integration test
* fix unit test + bring back connector in action of rule
Co-authored-by: Jonathan Buttner
Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Christos Nasikas
---
x-pack/plugins/case/common/api/cases/case.ts | 1 +
.../plugins/case/common/api/cases/comment.ts | 7 +-
.../case/common/api/cases/user_actions.ts | 1 +
x-pack/plugins/case/common/api/helpers.ts | 6 +
x-pack/plugins/case/common/constants.ts | 10 +
.../case/server/client/cases/create.test.ts | 3 +
.../plugins/case/server/client/cases/get.ts | 16 +-
.../plugins/case/server/client/cases/mock.ts | 4 +
.../case/server/client/cases/update.test.ts | 5 +
.../plugins/case/server/client/cases/utils.ts | 1 +
.../case/server/client/comments/add.test.ts | 24 ++
.../case/server/client/comments/add.ts | 29 ++-
x-pack/plugins/case/server/client/types.ts | 1 +
.../case/server/client/user_actions/get.ts | 30 ++-
.../plugins/case/server/common/utils.test.ts | 24 ++
.../case/server/connectors/case/index.test.ts | 4 +
.../case/server/connectors/case/index.ts | 51 +++-
.../case/server/connectors/case/schema.ts | 14 +-
.../plugins/case/server/connectors/index.ts | 24 ++
.../api/__fixtures__/mock_saved_objects.ts | 8 +
.../api/cases/comments/patch_comment.test.ts | 8 +
.../api/cases/comments/post_comment.test.ts | 4 +
.../routes/api/cases/patch_cases.test.ts | 3 +
.../server/routes/api/cases/post_case.test.ts | 1 +
.../user_actions/get_all_user_actions.ts | 35 ++-
.../plugins/case/server/routes/api/index.ts | 8 +-
.../case/server/routes/api/utils.test.ts | 8 +
.../plugins/case/server/routes/api/utils.ts | 7 +-
.../server/saved_object_types/comments.ts | 10 +
.../server/saved_object_types/migrations.ts | 6 +-
.../case/server/scripts/sub_cases/index.ts | 31 ++-
.../case/server/services/alerts/index.ts | 7 +-
.../server/services/user_actions/index.ts | 15 +-
.../security_solution/common/constants.ts | 2 +-
.../components/add_comment/index.test.tsx | 7 +-
.../cases/components/add_comment/index.tsx | 15 +-
.../cases/components/all_cases/actions.tsx | 64 ++---
.../cases/components/all_cases/columns.tsx | 45 +++-
.../components/all_cases/expanded_row.tsx | 68 +++++
.../cases/components/all_cases/helpers.ts | 37 +++
.../cases/components/all_cases/index.test.tsx | 4 +
.../cases/components/all_cases/index.tsx | 39 ++-
.../components/all_cases/translations.ts | 4 +
.../components/case_view/helpers.test.tsx | 22 +-
.../cases/components/case_view/helpers.ts | 41 +--
.../cases/components/case_view/index.test.tsx | 8 +
.../cases/components/case_view/index.tsx | 76 +-----
.../components/confirm_delete_case/index.tsx | 10 +-
.../confirm_delete_case/translations.ts | 5 +
.../connectors/case/alert_fields.tsx | 4 +-
.../connectors/case/existing_case.tsx | 101 ++++++--
.../connectors/case/translations.ts | 14 ++
.../cases/components/create/form_context.tsx | 11 +-
.../public/cases/components/create/mock.ts | 3 +-
.../add_to_case_action.test.tsx | 18 +-
.../timeline_actions/add_to_case_action.tsx | 19 +-
.../create_case_modal.tsx | 5 +-
.../use_create_case_modal/index.tsx | 10 +-
.../components/user_action_tree/helpers.tsx | 236 ++++++++++++++++--
.../components/user_action_tree/index.tsx | 97 +++++--
.../user_action_tree/translations.ts | 26 +-
.../user_action_alert_comment_event.test.tsx | 28 +--
.../user_action_alert_comment_event.tsx | 58 ++++-
.../user_action_copy_link.tsx | 14 +-
.../user_action_show_alert.test.tsx | 16 +-
.../user_action_show_alert.tsx | 23 +-
.../public/cases/containers/api.ts | 98 +++++++-
.../public/cases/containers/mock.ts | 8 +-
.../public/cases/containers/types.ts | 30 ++-
.../cases/containers/use_delete_cases.tsx | 10 +-
.../public/cases/containers/use_get_case.tsx | 40 +--
.../containers/use_get_case_user_actions.tsx | 56 +++--
.../containers/use_post_comment.test.tsx | 55 +++-
.../cases/containers/use_post_comment.tsx | 12 +-
.../cases/containers/use_update_case.test.tsx | 24 +-
.../cases/containers/use_update_case.tsx | 72 ++++--
.../containers/use_update_comment.test.tsx | 29 ++-
.../cases/containers/use_update_comment.tsx | 5 +-
.../public/cases/pages/case_details.tsx | 11 +-
.../public/cases/pages/index.tsx | 12 +-
.../components/link_to/redirect_to_case.tsx | 32 ++-
.../public/common/components/links/index.tsx | 9 +-
.../components/alerts_table/actions.test.tsx | 4 +-
.../components/alerts_table/actions.tsx | 236 +++++++++++++-----
.../investigate_in_timeline_action.tsx | 63 +++--
.../components/alerts_table/types.ts | 2 +-
.../detection_engine/alerts/use_query.tsx | 5 +-
.../signals/single_bulk_create.ts | 16 +-
.../translations/translations/ja-JP.json | 1 -
.../translations/translations/zh-CN.json | 1 -
.../tests/cases/comments/patch_comment.ts | 24 ++
.../tests/cases/comments/post_comment.ts | 16 ++
.../basic/tests/cases/patch_cases.ts | 16 ++
.../basic/tests/connectors/case.ts | 28 ++-
.../case_api_integration/common/lib/mock.ts | 8 +-
95 files changed, 1917 insertions(+), 542 deletions(-)
create mode 100644 x-pack/plugins/security_solution/public/cases/components/all_cases/expanded_row.tsx
create mode 100644 x-pack/plugins/security_solution/public/cases/components/all_cases/helpers.ts
diff --git a/x-pack/plugins/case/common/api/cases/case.ts b/x-pack/plugins/case/common/api/cases/case.ts
index 49643ca1f4d0..33a93952b0e2 100644
--- a/x-pack/plugins/case/common/api/cases/case.ts
+++ b/x-pack/plugins/case/common/api/cases/case.ts
@@ -123,6 +123,7 @@ export const CaseResponseRt = rt.intersection([
version: rt.string,
}),
rt.partial({
+ subCaseIds: rt.array(rt.string),
subCases: rt.array(SubCaseResponseRt),
comments: rt.array(CommentResponseRt),
}),
diff --git a/x-pack/plugins/case/common/api/cases/comment.ts b/x-pack/plugins/case/common/api/cases/comment.ts
index cfc6099fa4bb..41ad0e87f14d 100644
--- a/x-pack/plugins/case/common/api/cases/comment.ts
+++ b/x-pack/plugins/case/common/api/cases/comment.ts
@@ -52,7 +52,11 @@ export const ContextTypeUserRt = rt.type({
export const AlertCommentRequestRt = 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,
+ index: rt.union([rt.array(rt.string), rt.string]),
+ rule: rt.type({
+ id: rt.union([rt.string, rt.null]),
+ name: rt.union([rt.string, rt.null]),
+ }),
});
const AttributesTypeUserRt = rt.intersection([ContextTypeUserRt, CommentAttributesBasicRt]);
@@ -108,6 +112,7 @@ export const CommentsResponseRt = rt.type({
export const AllCommentsResponseRt = rt.array(CommentResponseRt);
+export type AttributesTypeAlerts = rt.TypeOf;
export type CommentAttributes = rt.TypeOf;
export type CommentRequest = rt.TypeOf;
export type CommentResponse = rt.TypeOf;
diff --git a/x-pack/plugins/case/common/api/cases/user_actions.ts b/x-pack/plugins/case/common/api/cases/user_actions.ts
index de9e88993df9..6c8e0de80903 100644
--- a/x-pack/plugins/case/common/api/cases/user_actions.ts
+++ b/x-pack/plugins/case/common/api/cases/user_actions.ts
@@ -49,6 +49,7 @@ const CaseUserActionResponseRT = rt.intersection([
case_id: rt.string,
comment_id: rt.union([rt.string, rt.null]),
}),
+ rt.partial({ sub_case_id: rt.string }),
]);
export const CaseUserActionAttributesRt = CaseUserActionBasicRT;
diff --git a/x-pack/plugins/case/common/api/helpers.ts b/x-pack/plugins/case/common/api/helpers.ts
index 9c290c0a4d61..00c8ff402c80 100644
--- a/x-pack/plugins/case/common/api/helpers.ts
+++ b/x-pack/plugins/case/common/api/helpers.ts
@@ -13,6 +13,7 @@ import {
SUB_CASE_DETAILS_URL,
SUB_CASES_URL,
CASE_PUSH_URL,
+ SUB_CASE_USER_ACTIONS_URL,
} from '../constants';
export const getCaseDetailsUrl = (id: string): string => {
@@ -38,6 +39,11 @@ export const getCaseCommentDetailsUrl = (caseId: string, commentId: string): str
export const getCaseUserActionUrl = (id: string): string => {
return CASE_USER_ACTIONS_URL.replace('{case_id}', id);
};
+
+export const getSubCaseUserActionUrl = (caseID: string, subCaseID: string): string => {
+ return SUB_CASE_USER_ACTIONS_URL.replace('{case_id}', caseID).replace('{sub_case_id}', subCaseID);
+};
+
export const getCasePushUrl = (caseId: string, connectorId: string): string => {
return CASE_PUSH_URL.replace('{case_id}', caseId).replace('{connector_id}', connectorId);
};
diff --git a/x-pack/plugins/case/common/constants.ts b/x-pack/plugins/case/common/constants.ts
index 5d34ed120ff6..cc69c7ecc290 100644
--- a/x-pack/plugins/case/common/constants.ts
+++ b/x-pack/plugins/case/common/constants.ts
@@ -5,6 +5,8 @@
* 2.0.
*/
+import { DEFAULT_MAX_SIGNALS } from '../../security_solution/common/constants';
+
export const APP_ID = 'case';
/**
@@ -19,6 +21,7 @@ export const CASE_CONFIGURE_CONNECTORS_URL = `${CASE_CONFIGURE_URL}/connectors`;
export const SUB_CASES_PATCH_DEL_URL = `${CASES_URL}/sub_cases`;
export const SUB_CASES_URL = `${CASE_DETAILS_URL}/sub_cases`;
export const SUB_CASE_DETAILS_URL = `${CASE_DETAILS_URL}/sub_cases/{sub_case_id}`;
+export const SUB_CASE_USER_ACTIONS_URL = `${SUB_CASE_DETAILS_URL}/user_actions`;
export const CASE_COMMENTS_URL = `${CASE_DETAILS_URL}/comments`;
export const CASE_COMMENT_DETAILS_URL = `${CASE_DETAILS_URL}/comments/{comment_id}`;
@@ -45,3 +48,10 @@ export const SUPPORTED_CONNECTORS = [
JIRA_ACTION_TYPE_ID,
RESILIENT_ACTION_TYPE_ID,
];
+
+/**
+ * Alerts
+ */
+
+export const MAX_ALERTS_PER_SUB_CASE = 5000;
+export const MAX_GENERATED_ALERTS_PER_SUB_CASE = MAX_ALERTS_PER_SUB_CASE / DEFAULT_MAX_SIGNALS;
diff --git a/x-pack/plugins/case/server/client/cases/create.test.ts b/x-pack/plugins/case/server/client/cases/create.test.ts
index 065825472954..3016a57f2187 100644
--- a/x-pack/plugins/case/server/client/cases/create.test.ts
+++ b/x-pack/plugins/case/server/client/cases/create.test.ts
@@ -76,6 +76,7 @@ describe('create', () => {
"syncAlerts": true,
},
"status": "open",
+ "subCaseIds": undefined,
"subCases": undefined,
"tags": Array [
"defacement",
@@ -174,6 +175,7 @@ describe('create', () => {
"syncAlerts": true,
},
"status": "open",
+ "subCaseIds": undefined,
"subCases": undefined,
"tags": Array [
"defacement",
@@ -239,6 +241,7 @@ describe('create', () => {
"syncAlerts": true,
},
"status": "open",
+ "subCaseIds": undefined,
"subCases": undefined,
"tags": Array [
"defacement",
diff --git a/x-pack/plugins/case/server/client/cases/get.ts b/x-pack/plugins/case/server/client/cases/get.ts
index eab43a0c4d45..ab0b97abbcb7 100644
--- a/x-pack/plugins/case/server/client/cases/get.ts
+++ b/x-pack/plugins/case/server/client/cases/get.ts
@@ -26,19 +26,24 @@ export const get = async ({
includeComments = false,
includeSubCaseComments = false,
}: GetParams): Promise => {
- const theCase = await caseService.getCase({
- client: savedObjectsClient,
- id,
- });
+ const [theCase, subCasesForCaseId] = await Promise.all([
+ caseService.getCase({
+ client: savedObjectsClient,
+ id,
+ }),
+ caseService.findSubCasesByCaseId({ client: savedObjectsClient, ids: [id] }),
+ ]);
+
+ const subCaseIds = subCasesForCaseId.saved_objects.map((so) => so.id);
if (!includeComments) {
return CaseResponseRt.encode(
flattenCaseSavedObject({
savedObject: theCase,
+ subCaseIds,
})
);
}
-
const theComments = await caseService.getAllCaseComments({
client: savedObjectsClient,
id,
@@ -53,6 +58,7 @@ export const get = async ({
flattenCaseSavedObject({
savedObject: theCase,
comments: theComments.saved_objects,
+ subCaseIds,
totalComment: theComments.total,
totalAlerts: countAlertsForID({ comments: theComments, id }),
})
diff --git a/x-pack/plugins/case/server/client/cases/mock.ts b/x-pack/plugins/case/server/client/cases/mock.ts
index 2be9f4105983..809c4ad1ea1b 100644
--- a/x-pack/plugins/case/server/client/cases/mock.ts
+++ b/x-pack/plugins/case/server/client/cases/mock.ts
@@ -54,6 +54,10 @@ export const commentAlert: CommentResponse = {
id: 'mock-comment-1',
alertId: 'alert-id-1',
index: 'alert-index-1',
+ rule: {
+ id: 'rule-id-1',
+ name: 'rule-name-1',
+ },
type: CommentType.alert as const,
created_at: '2019-11-25T21:55:00.177Z',
created_by: {
diff --git a/x-pack/plugins/case/server/client/cases/update.test.ts b/x-pack/plugins/case/server/client/cases/update.test.ts
index 53e233c74deb..7a3e4458f25c 100644
--- a/x-pack/plugins/case/server/client/cases/update.test.ts
+++ b/x-pack/plugins/case/server/client/cases/update.test.ts
@@ -71,6 +71,7 @@ describe('update', () => {
"syncAlerts": true,
},
"status": "closed",
+ "subCaseIds": undefined,
"subCases": undefined,
"tags": Array [
"defacement",
@@ -166,6 +167,7 @@ describe('update', () => {
"syncAlerts": true,
},
"status": "open",
+ "subCaseIds": undefined,
"subCases": undefined,
"tags": Array [
"defacement",
@@ -233,6 +235,7 @@ describe('update', () => {
"syncAlerts": true,
},
"status": "in-progress",
+ "subCaseIds": undefined,
"subCases": undefined,
"tags": Array [
"LOLBins",
@@ -300,6 +303,7 @@ describe('update', () => {
"syncAlerts": true,
},
"status": "closed",
+ "subCaseIds": undefined,
"subCases": undefined,
"tags": Array [
"defacement",
@@ -371,6 +375,7 @@ describe('update', () => {
"syncAlerts": true,
},
"status": "open",
+ "subCaseIds": undefined,
"subCases": undefined,
"tags": Array [
"LOLBins",
diff --git a/x-pack/plugins/case/server/client/cases/utils.ts b/x-pack/plugins/case/server/client/cases/utils.ts
index 78bdc6d282c6..fda4142bf77c 100644
--- a/x-pack/plugins/case/server/client/cases/utils.ts
+++ b/x-pack/plugins/case/server/client/cases/utils.ts
@@ -314,6 +314,7 @@ export const getCommentContextFromAttributes = (
type: attributes.type,
alertId: attributes.alertId,
index: attributes.index,
+ rule: attributes.rule,
};
default:
return {
diff --git a/x-pack/plugins/case/server/client/comments/add.test.ts b/x-pack/plugins/case/server/client/comments/add.test.ts
index 315203a1f5e1..c9b1e4fd1327 100644
--- a/x-pack/plugins/case/server/client/comments/add.test.ts
+++ b/x-pack/plugins/case/server/client/comments/add.test.ts
@@ -75,6 +75,10 @@ describe('addComment', () => {
type: CommentType.alert,
alertId: 'test-id',
index: 'test-index',
+ rule: {
+ id: 'test-rule1',
+ name: 'test-rule',
+ },
},
});
@@ -94,6 +98,10 @@ describe('addComment', () => {
"index": "test-index",
"pushed_at": null,
"pushed_by": null,
+ "rule": Object {
+ "id": "test-rule1",
+ "name": "test-rule",
+ },
"type": "alert",
"updated_at": null,
"updated_by": null,
@@ -231,6 +239,10 @@ describe('addComment', () => {
type: CommentType.alert,
alertId: 'test-alert',
index: 'test-index',
+ rule: {
+ id: 'test-rule1',
+ name: 'test-rule',
+ },
},
});
@@ -265,6 +277,10 @@ describe('addComment', () => {
type: CommentType.alert,
alertId: 'test-alert',
index: 'test-index',
+ rule: {
+ id: 'test-rule1',
+ name: 'test-rule',
+ },
},
});
@@ -406,6 +422,10 @@ describe('addComment', () => {
type: CommentType.alert,
index: 'test-index',
alertId: 'test-id',
+ rule: {
+ id: 'test-rule1',
+ name: 'test-rule',
+ },
},
})
.catch((e) => {
@@ -478,6 +498,10 @@ describe('addComment', () => {
type: CommentType.alert,
alertId: 'test-alert',
index: 'test-index',
+ rule: {
+ id: 'test-rule1',
+ name: 'test-rule',
+ },
},
})
.catch((e) => {
diff --git a/x-pack/plugins/case/server/client/comments/add.ts b/x-pack/plugins/case/server/client/comments/add.ts
index 7dd1b4a8f6c5..0a86c1825fed 100644
--- a/x-pack/plugins/case/server/client/comments/add.ts
+++ b/x-pack/plugins/case/server/client/comments/add.ts
@@ -38,6 +38,8 @@ import {
import { CaseServiceSetup, CaseUserActionServiceSetup } from '../../services';
import { CommentableCase } from '../../common';
import { CaseClientHandler } from '..';
+import { CASE_COMMENT_SAVED_OBJECT } from '../../saved_object_types';
+import { MAX_GENERATED_ALERTS_PER_SUB_CASE } from '../../../common/constants';
async function getSubCase({
caseService,
@@ -56,7 +58,20 @@ async function getSubCase({
}): Promise> {
const mostRecentSubCase = await caseService.getMostRecentSubCase(savedObjectsClient, caseId);
if (mostRecentSubCase && mostRecentSubCase.attributes.status !== CaseStatuses.closed) {
- return mostRecentSubCase;
+ const subCaseAlertsAttachement = await caseService.getAllSubCaseComments({
+ client: savedObjectsClient,
+ id: mostRecentSubCase.id,
+ options: {
+ fields: [],
+ filter: `${CASE_COMMENT_SAVED_OBJECT}.attributes.type: ${CommentType.generatedAlert}`,
+ page: 1,
+ perPage: 1,
+ },
+ });
+
+ if (subCaseAlertsAttachement.total <= MAX_GENERATED_ALERTS_PER_SUB_CASE) {
+ return mostRecentSubCase;
+ }
}
const newSubCase = await caseService.createSubCase({
@@ -160,7 +175,11 @@ const addGeneratedAlerts = async ({
await caseClient.updateAlertsStatus({
ids,
status: subCase.attributes.status,
- indices: new Set([newComment.attributes.index]),
+ indices: new Set([
+ ...(Array.isArray(newComment.attributes.index)
+ ? newComment.attributes.index
+ : [newComment.attributes.index]),
+ ]),
});
}
@@ -282,7 +301,11 @@ export const addComment = async ({
await caseClient.updateAlertsStatus({
ids,
status: updatedCase.status,
- indices: new Set([newComment.attributes.index]),
+ indices: new Set([
+ ...(Array.isArray(newComment.attributes.index)
+ ? newComment.attributes.index
+ : [newComment.attributes.index]),
+ ]),
});
}
diff --git a/x-pack/plugins/case/server/client/types.ts b/x-pack/plugins/case/server/client/types.ts
index a8f64227daf8..ba5677426c22 100644
--- a/x-pack/plugins/case/server/client/types.ts
+++ b/x-pack/plugins/case/server/client/types.ts
@@ -59,6 +59,7 @@ export interface CaseClientGetAlerts {
export interface CaseClientGetUserActions {
caseId: string;
+ subCaseId?: string;
}
export interface MappingsClient {
diff --git a/x-pack/plugins/case/server/client/user_actions/get.ts b/x-pack/plugins/case/server/client/user_actions/get.ts
index 8a4e45f71b9c..f6371b8e8b1e 100644
--- a/x-pack/plugins/case/server/client/user_actions/get.ts
+++ b/x-pack/plugins/case/server/client/user_actions/get.ts
@@ -6,7 +6,11 @@
*/
import { SavedObjectsClientContract } from 'kibana/server';
-import { CASE_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT } from '../../saved_object_types';
+import {
+ CASE_SAVED_OBJECT,
+ CASE_COMMENT_SAVED_OBJECT,
+ SUB_CASE_SAVED_OBJECT,
+} from '../../saved_object_types';
import { CaseUserActionsResponseRt, CaseUserActionsResponse } from '../../../common/api';
import { CaseUserActionServiceSetup } from '../../services';
@@ -14,24 +18,36 @@ interface GetParams {
savedObjectsClient: SavedObjectsClientContract;
userActionService: CaseUserActionServiceSetup;
caseId: string;
+ subCaseId?: string;
}
export const get = async ({
savedObjectsClient,
userActionService,
caseId,
+ subCaseId,
}: GetParams): Promise => {
const userActions = await userActionService.getUserActions({
client: savedObjectsClient,
caseId,
+ subCaseId,
});
return CaseUserActionsResponseRt.encode(
- userActions.saved_objects.map((ua) => ({
- ...ua.attributes,
- action_id: ua.id,
- case_id: ua.references.find((r) => r.type === CASE_SAVED_OBJECT)?.id ?? '',
- comment_id: ua.references.find((r) => r.type === CASE_COMMENT_SAVED_OBJECT)?.id ?? null,
- }))
+ userActions.saved_objects.reduce((acc, ua) => {
+ if (subCaseId == null && ua.references.some((uar) => uar.type === SUB_CASE_SAVED_OBJECT)) {
+ return acc;
+ }
+ return [
+ ...acc,
+ {
+ ...ua.attributes,
+ action_id: ua.id,
+ case_id: ua.references.find((r) => r.type === CASE_SAVED_OBJECT)?.id ?? '',
+ comment_id: ua.references.find((r) => r.type === CASE_COMMENT_SAVED_OBJECT)?.id ?? null,
+ sub_case_id: ua.references.find((r) => r.type === SUB_CASE_SAVED_OBJECT)?.id ?? '',
+ },
+ ];
+ }, [])
);
};
diff --git a/x-pack/plugins/case/server/common/utils.test.ts b/x-pack/plugins/case/server/common/utils.test.ts
index d89feb009f80..5e6a86358de2 100644
--- a/x-pack/plugins/case/server/common/utils.test.ts
+++ b/x-pack/plugins/case/server/common/utils.test.ts
@@ -99,6 +99,10 @@ describe('common utils', () => {
alertId: ['a', 'b', 'c'],
index: '',
type: CommentType.generatedAlert,
+ rule: {
+ id: 'rule-id-1',
+ name: 'rule-name-1',
+ },
},
],
},
@@ -118,6 +122,10 @@ describe('common utils', () => {
alertId: ['a', 'b', 'c'],
index: '',
type: CommentType.alert,
+ rule: {
+ id: 'rule-id-1',
+ name: 'rule-name-1',
+ },
},
],
},
@@ -139,6 +147,10 @@ describe('common utils', () => {
alertId: ['a', 'b'],
index: '',
type: CommentType.alert,
+ rule: {
+ id: 'rule-id-1',
+ name: 'rule-name-1',
+ },
},
{
comment: '',
@@ -164,6 +176,10 @@ describe('common utils', () => {
alertId: ['a', 'b'],
index: '',
type: CommentType.alert,
+ rule: {
+ id: 'rule-id-1',
+ name: 'rule-name-1',
+ },
},
],
},
@@ -197,6 +213,10 @@ describe('common utils', () => {
alertId: ['a', 'b'],
index: '',
type: CommentType.alert,
+ rule: {
+ id: 'rule-id-1',
+ name: 'rule-name-1',
+ },
},
],
},
@@ -224,6 +244,10 @@ describe('common utils', () => {
alertId: ['a', 'b'],
index: '',
type: CommentType.alert,
+ rule: {
+ id: 'rule-id-1',
+ name: 'rule-name-1',
+ },
},
],
},
diff --git a/x-pack/plugins/case/server/connectors/case/index.test.ts b/x-pack/plugins/case/server/connectors/case/index.test.ts
index 6b7e395bae4d..4be519858db1 100644
--- a/x-pack/plugins/case/server/connectors/case/index.test.ts
+++ b/x-pack/plugins/case/server/connectors/case/index.test.ts
@@ -717,6 +717,10 @@ describe('case connector', () => {
type: CommentType.alert,
alertId: 'test-id',
index: 'test-index',
+ rule: {
+ id: null,
+ name: null,
+ },
},
},
};
diff --git a/x-pack/plugins/case/server/connectors/case/index.ts b/x-pack/plugins/case/server/connectors/case/index.ts
index 34b407616cfe..a64cba567ce4 100644
--- a/x-pack/plugins/case/server/connectors/case/index.ts
+++ b/x-pack/plugins/case/server/connectors/case/index.ts
@@ -122,23 +122,48 @@ async function executor(
/**
* This converts a connector style generated alert ({_id: string} | {_id: string}[]) to the expected format of addComment.
*/
+interface AttachmentAlerts {
+ ids: string[];
+ indices: string[];
+ rule: { id: string | null; name: string | null };
+}
export const transformConnectorComment = (comment: CommentSchemaType): CommentRequest => {
if (isCommentGeneratedAlert(comment)) {
- const alertId: string[] = [];
- if (Array.isArray(comment.alerts)) {
- alertId.push(
- ...comment.alerts.map((alert: { _id: string }) => {
- return alert._id;
- })
+ try {
+ const genAlerts: Array<{
+ _id: string;
+ _index: string;
+ ruleId: string | undefined;
+ ruleName: string | undefined;
+ }> = JSON.parse(
+ `${comment.alerts.substring(0, comment.alerts.lastIndexOf('__SEPARATOR__'))}]`.replace(
+ /__SEPARATOR__/gi,
+ ','
+ )
);
- } else {
- alertId.push(comment.alerts._id);
+
+ const { ids, indices, rule } = genAlerts.reduce(
+ (acc, { _id, _index, ruleId, ruleName }) => {
+ // Mutation is faster than destructing.
+ // Mutation usually leads to side effects but for this scenario it's ok to do it.
+ acc.ids.push(_id);
+ acc.indices.push(_index);
+ // We assume one rule per batch of alerts
+ acc.rule = { id: ruleId ?? null, name: ruleName ?? null };
+ return acc;
+ },
+ { ids: [], indices: [], rule: { id: null, name: null } }
+ );
+
+ return {
+ type: CommentType.generatedAlert,
+ alertId: ids,
+ index: indices,
+ rule,
+ };
+ } catch (e) {
+ throw new Error(`Error parsing generated alert in case connector -> ${e.message}`);
}
- return {
- type: CommentType.generatedAlert,
- alertId,
- index: comment.index,
- };
} else {
return comment;
}
diff --git a/x-pack/plugins/case/server/connectors/case/schema.ts b/x-pack/plugins/case/server/connectors/case/schema.ts
index cdeb00209f84..ac34ad40cfa1 100644
--- a/x-pack/plugins/case/server/connectors/case/schema.ts
+++ b/x-pack/plugins/case/server/connectors/case/schema.ts
@@ -17,17 +17,9 @@ const ContextTypeUserSchema = schema.object({
comment: schema.string(),
});
-const AlertIDSchema = schema.object(
- {
- _id: schema.string(),
- },
- { unknowns: 'ignore' }
-);
-
const ContextTypeAlertGroupSchema = schema.object({
type: schema.literal(CommentType.generatedAlert),
- alerts: schema.oneOf([schema.arrayOf(AlertIDSchema), AlertIDSchema]),
- index: schema.string(),
+ alerts: schema.string(),
});
export type ContextTypeGeneratedAlertType = typeof ContextTypeAlertGroupSchema.type;
@@ -37,6 +29,10 @@ const ContextTypeAlertSchema = schema.object({
// allowing either an array or a single value to preserve the previous API of attaching a single alert ID
alertId: schema.oneOf([schema.arrayOf(schema.string()), schema.string()]),
index: schema.string(),
+ rule: schema.object({
+ id: schema.nullable(schema.string()),
+ name: schema.nullable(schema.string()),
+ }),
});
export type ContextTypeAlertSchemaType = typeof ContextTypeAlertSchema.type;
diff --git a/x-pack/plugins/case/server/connectors/index.ts b/x-pack/plugins/case/server/connectors/index.ts
index 056ccff2733a..898d61301a14 100644
--- a/x-pack/plugins/case/server/connectors/index.ts
+++ b/x-pack/plugins/case/server/connectors/index.ts
@@ -65,3 +65,27 @@ export const isCommentAlert = (
): comment is ContextTypeAlertSchemaType => {
return comment.type === CommentType.alert;
};
+
+/**
+ * Separator field for the case connector alerts string parser.
+ */
+const separator = '__SEPARATOR__';
+
+interface AlertIDIndex {
+ _id: string;
+ _index: string;
+ ruleId: string;
+ ruleName: string;
+}
+
+/**
+ * Creates the format that the connector's parser is expecting, it should result in something like this:
+ * [{"_id":"1","_index":"index1"}__SEPARATOR__{"_id":"id2","_index":"index2"}__SEPARATOR__]
+ *
+ * This should only be used for testing purposes.
+ */
+export function createAlertsString(alerts: AlertIDIndex[]) {
+ return `[${alerts.reduce((acc, alert) => {
+ return `${acc}${JSON.stringify(alert)}${separator}`;
+ }, '')}]`;
+}
diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts
index 2fe0be3e08ed..e67a6f6dd334 100644
--- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts
+++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts
@@ -346,6 +346,10 @@ export const mockCaseComments: Array> = [
},
pushed_at: null,
pushed_by: null,
+ rule: {
+ id: 'rule-id-1',
+ name: 'rule-name-1',
+ },
updated_at: '2019-11-25T22:32:30.608Z',
updated_by: {
full_name: 'elastic',
@@ -379,6 +383,10 @@ export const mockCaseComments: Array> = [
},
pushed_at: null,
pushed_by: null,
+ rule: {
+ id: 'rule-id-2',
+ name: 'rule-name-2',
+ },
updated_at: '2019-11-25T22:32:30.608Z',
updated_by: {
full_name: 'elastic',
diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts
index 9dec910f9fc4..1ebd336c83af 100644
--- a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts
@@ -69,6 +69,10 @@ describe('PATCH comment', () => {
type: CommentType.alert,
alertId: 'new-id',
index: 'test-index',
+ rule: {
+ id: 'rule-id',
+ name: 'rule',
+ },
id: commentID,
version: 'WzYsMV0=',
},
@@ -218,6 +222,10 @@ describe('PATCH comment', () => {
type: CommentType.alert,
alertId: 'test-id',
index: 'test-index',
+ rule: {
+ id: 'rule-id',
+ name: 'rule',
+ },
id: 'mock-comment-1',
version: 'WzEsMV0=',
},
diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts
index fb51b8f76d0e..807ec0d089a5 100644
--- a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts
@@ -68,6 +68,10 @@ describe('POST comment', () => {
type: CommentType.alert,
alertId: 'test-id',
index: 'test-index',
+ rule: {
+ id: 'rule-id',
+ name: 'rule-name',
+ },
},
});
diff --git a/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts b/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts
index e50d14e5c66c..b3f87211c954 100644
--- a/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts
@@ -81,6 +81,7 @@ describe('PATCH cases', () => {
"syncAlerts": true,
},
"status": "closed",
+ "subCaseIds": undefined,
"subCases": undefined,
"tags": Array [
"defacement",
@@ -154,6 +155,7 @@ describe('PATCH cases', () => {
"syncAlerts": true,
},
"status": "open",
+ "subCaseIds": undefined,
"subCases": undefined,
"tags": Array [
"LOLBins",
@@ -222,6 +224,7 @@ describe('PATCH cases', () => {
"syncAlerts": true,
},
"status": "in-progress",
+ "subCaseIds": undefined,
"subCases": undefined,
"tags": Array [
"defacement",
diff --git a/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts
index 53829157c5b0..e1669203d3de 100644
--- a/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts
@@ -213,6 +213,7 @@ describe('POST cases', () => {
"syncAlerts": true,
},
"status": "open",
+ "subCaseIds": undefined,
"subCases": undefined,
"tags": Array [
"defacement",
diff --git a/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts b/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts
index 06e929cc40e6..488f32a79581 100644
--- a/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts
@@ -9,9 +9,9 @@ import { schema } from '@kbn/config-schema';
import { RouteDeps } from '../../types';
import { wrapError } from '../../utils';
-import { CASE_USER_ACTIONS_URL } from '../../../../../common/constants';
+import { CASE_USER_ACTIONS_URL, SUB_CASE_USER_ACTIONS_URL } from '../../../../../common/constants';
-export function initGetAllUserActionsApi({ router }: RouteDeps) {
+export function initGetAllCaseUserActionsApi({ router }: RouteDeps) {
router.get(
{
path: CASE_USER_ACTIONS_URL,
@@ -39,3 +39,34 @@ export function initGetAllUserActionsApi({ router }: RouteDeps) {
}
);
}
+
+export function initGetAllSubCaseUserActionsApi({ router }: RouteDeps) {
+ router.get(
+ {
+ path: SUB_CASE_USER_ACTIONS_URL,
+ validate: {
+ params: schema.object({
+ case_id: schema.string(),
+ sub_case_id: schema.string(),
+ }),
+ },
+ },
+ async (context, request, response) => {
+ if (!context.case) {
+ return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' });
+ }
+
+ const caseClient = context.case.getCaseClient();
+ const caseId = request.params.case_id;
+ const subCaseId = request.params.sub_case_id;
+
+ try {
+ return response.ok({
+ body: await caseClient.getUserActions({ caseId, subCaseId }),
+ });
+ } catch (error) {
+ return response.customError(wrapError(error));
+ }
+ }
+ );
+}
diff --git a/x-pack/plugins/case/server/routes/api/index.ts b/x-pack/plugins/case/server/routes/api/index.ts
index f2fd986dd8a3..12d1da36077c 100644
--- a/x-pack/plugins/case/server/routes/api/index.ts
+++ b/x-pack/plugins/case/server/routes/api/index.ts
@@ -14,7 +14,10 @@ import { initPushCaseApi } from './cases/push_case';
import { initGetReportersApi } from './cases/reporters/get_reporters';
import { initGetCasesStatusApi } from './cases/status/get_status';
import { initGetTagsApi } from './cases/tags/get_tags';
-import { initGetAllUserActionsApi } from './cases/user_actions/get_all_user_actions';
+import {
+ initGetAllCaseUserActionsApi,
+ initGetAllSubCaseUserActionsApi,
+} from './cases/user_actions/get_all_user_actions';
import { initDeleteCommentApi } from './cases/comments/delete_comment';
import { initDeleteAllCommentsApi } from './cases/comments/delete_all_comments';
@@ -52,7 +55,8 @@ export function initCaseApi(deps: RouteDeps) {
initPatchCasesApi(deps);
initPostCaseApi(deps);
initPushCaseApi(deps);
- initGetAllUserActionsApi(deps);
+ initGetAllCaseUserActionsApi(deps);
+ initGetAllSubCaseUserActionsApi(deps);
// Sub cases
initGetSubCaseApi(deps);
initPatchSubCasesApi(deps);
diff --git a/x-pack/plugins/case/server/routes/api/utils.test.ts b/x-pack/plugins/case/server/routes/api/utils.test.ts
index 1efec927efb6..f6bc1e4f7189 100644
--- a/x-pack/plugins/case/server/routes/api/utils.test.ts
+++ b/x-pack/plugins/case/server/routes/api/utils.test.ts
@@ -401,6 +401,7 @@ describe('Utils', () => {
"syncAlerts": true,
},
"status": "open",
+ "subCaseIds": undefined,
"subCases": undefined,
"tags": Array [
"defacement",
@@ -440,6 +441,7 @@ describe('Utils', () => {
"syncAlerts": true,
},
"status": "open",
+ "subCaseIds": undefined,
"subCases": undefined,
"tags": Array [
"Data Destruction",
@@ -483,6 +485,7 @@ describe('Utils', () => {
"syncAlerts": true,
},
"status": "open",
+ "subCaseIds": undefined,
"subCases": undefined,
"tags": Array [
"LOLBins",
@@ -530,6 +533,7 @@ describe('Utils', () => {
"syncAlerts": true,
},
"status": "closed",
+ "subCaseIds": undefined,
"subCases": undefined,
"tags": Array [
"LOLBins",
@@ -594,6 +598,7 @@ describe('Utils', () => {
"syncAlerts": true,
},
"status": "open",
+ "subCaseIds": undefined,
"subCases": undefined,
"tags": Array [
"LOLBins",
@@ -649,6 +654,7 @@ describe('Utils', () => {
"syncAlerts": true,
},
"status": "open",
+ "subCaseIds": undefined,
"subCases": undefined,
"tags": Array [
"LOLBins",
@@ -727,6 +733,7 @@ describe('Utils', () => {
"syncAlerts": true,
},
"status": "open",
+ "subCaseIds": undefined,
"subCases": undefined,
"tags": Array [
"LOLBins",
@@ -781,6 +788,7 @@ describe('Utils', () => {
"syncAlerts": true,
},
"status": "open",
+ "subCaseIds": undefined,
"subCases": undefined,
"tags": Array [
"defacement",
diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts
index bc82f656f477..084b1a17a143 100644
--- a/x-pack/plugins/case/server/routes/api/utils.ts
+++ b/x-pack/plugins/case/server/routes/api/utils.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import { isEmpty } from 'lodash';
import { badRequest, boomify, isBoom } from '@hapi/boom';
import { fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';
@@ -120,7 +121,8 @@ export interface AlertInfo {
const accumulateIndicesAndIDs = (comment: CommentAttributes, acc: AlertInfo): AlertInfo => {
if (isCommentRequestTypeAlertOrGenAlert(comment)) {
acc.ids.push(...getAlertIds(comment));
- acc.indices.add(comment.index);
+ const indices = Array.isArray(comment.index) ? comment.index : [comment.index];
+ indices.forEach((index) => acc.indices.add(index));
}
return acc;
};
@@ -249,12 +251,14 @@ export const flattenCaseSavedObject = ({
totalComment = comments.length,
totalAlerts = 0,
subCases,
+ subCaseIds,
}: {
savedObject: SavedObject;
comments?: Array>;
totalComment?: number;
totalAlerts?: number;
subCases?: SubCaseResponse[];
+ subCaseIds?: string[];
}): CaseResponse => ({
id: savedObject.id,
version: savedObject.version ?? '0',
@@ -264,6 +268,7 @@ export const flattenCaseSavedObject = ({
...savedObject.attributes,
connector: transformESConnectorToCaseConnector(savedObject.attributes.connector),
subCases,
+ subCaseIds: !isEmpty(subCaseIds) ? subCaseIds : undefined,
});
export const flattenSubCaseSavedObject = ({
diff --git a/x-pack/plugins/case/server/saved_object_types/comments.ts b/x-pack/plugins/case/server/saved_object_types/comments.ts
index 9eabf744f2e1..a4fdc24b6e4e 100644
--- a/x-pack/plugins/case/server/saved_object_types/comments.ts
+++ b/x-pack/plugins/case/server/saved_object_types/comments.ts
@@ -63,6 +63,16 @@ export const caseCommentSavedObjectType: SavedObjectsType = {
},
},
},
+ rule: {
+ properties: {
+ id: {
+ type: 'keyword',
+ },
+ name: {
+ type: 'keyword',
+ },
+ },
+ },
updated_at: {
type: 'date',
},
diff --git a/x-pack/plugins/case/server/saved_object_types/migrations.ts b/x-pack/plugins/case/server/saved_object_types/migrations.ts
index a0b22c49d0bc..21ef27de1ec8 100644
--- a/x-pack/plugins/case/server/saved_object_types/migrations.ts
+++ b/x-pack/plugins/case/server/saved_object_types/migrations.ts
@@ -173,8 +173,9 @@ interface SanitizedComment {
type: CommentType;
}
-interface SanitizedCommentAssociationType {
+interface SanitizedCommentFoSubCases {
associationType: AssociationType;
+ rule: { id: string | null; name: string | null };
}
export const commentsMigrations = {
@@ -192,11 +193,12 @@ export const commentsMigrations = {
},
'7.12.0': (
doc: SavedObjectUnsanitizedDoc
- ): SavedObjectSanitizedDoc => {
+ ): SavedObjectSanitizedDoc => {
return {
...doc,
attributes: {
...doc.attributes,
+ rule: { id: null, name: null },
associationType: AssociationType.case,
},
references: doc.references || [],
diff --git a/x-pack/plugins/case/server/scripts/sub_cases/index.ts b/x-pack/plugins/case/server/scripts/sub_cases/index.ts
index 2ea9718d1848..9dd577c40c74 100644
--- a/x-pack/plugins/case/server/scripts/sub_cases/index.ts
+++ b/x-pack/plugins/case/server/scripts/sub_cases/index.ts
@@ -16,6 +16,7 @@ import {
import { CommentType } from '../../../common/api/cases/comment';
import { CASES_URL } from '../../../common/constants';
import { ActionResult, ActionTypeExecutorResult } from '../../../../actions/common';
+import { ContextTypeGeneratedAlertType, createAlertsString } from '../../connectors';
main();
@@ -105,6 +106,18 @@ async function handleGenGroupAlerts(argv: any) {
}
console.log('Case id: ', caseID);
+ const comment: ContextTypeGeneratedAlertType = {
+ type: CommentType.generatedAlert,
+ alerts: createAlertsString(
+ argv.ids.map((id: string) => ({
+ _id: id,
+ _index: argv.signalsIndex,
+ ruleId: argv.ruleID,
+ ruleName: argv.ruleName,
+ }))
+ ),
+ };
+
const executeResp = await client.request<
ActionTypeExecutorResult
>({
@@ -115,11 +128,7 @@ async function handleGenGroupAlerts(argv: any) {
subAction: 'addComment',
subActionParams: {
caseId: caseID,
- comment: {
- type: CommentType.generatedAlert,
- alerts: argv.ids.map((id: string) => ({ _id: id })),
- index: argv.signalsIndex,
- },
+ comment,
},
},
},
@@ -175,6 +184,18 @@ async function main() {
type: 'string',
default: '.siem-signals-default',
},
+ ruleID: {
+ alias: 'ri',
+ describe: 'siem signals rule id',
+ type: 'string',
+ default: 'rule-id',
+ },
+ ruleName: {
+ alias: 'rn',
+ describe: 'siem signals rule name',
+ type: 'string',
+ default: 'rule-name',
+ },
})
.demandOption(['ids']);
},
diff --git a/x-pack/plugins/case/server/services/alerts/index.ts b/x-pack/plugins/case/server/services/alerts/index.ts
index 320d32ac0d78..a19e533418bc 100644
--- a/x-pack/plugins/case/server/services/alerts/index.ts
+++ b/x-pack/plugins/case/server/services/alerts/index.ts
@@ -11,6 +11,7 @@ import type { PublicMethodsOf } from '@kbn/utility-types';
import { ElasticsearchClient } from 'kibana/server';
import { CaseStatuses } from '../../../common/api';
+import { MAX_ALERTS_PER_SUB_CASE } from '../../../common/constants';
export type AlertServiceContract = PublicMethodsOf;
@@ -95,14 +96,14 @@ export class AlertService {
query: {
bool: {
filter: {
- bool: {
- should: ids.map((_id) => ({ match: { _id } })),
- minimum_should_match: 1,
+ ids: {
+ values: ids,
},
},
},
},
},
+ size: MAX_ALERTS_PER_SUB_CASE,
ignore_unavailable: true,
});
diff --git a/x-pack/plugins/case/server/services/user_actions/index.ts b/x-pack/plugins/case/server/services/user_actions/index.ts
index 091775827c6a..d05ada0dba30 100644
--- a/x-pack/plugins/case/server/services/user_actions/index.ts
+++ b/x-pack/plugins/case/server/services/user_actions/index.ts
@@ -13,11 +13,16 @@ import {
} from 'kibana/server';
import { CaseUserActionAttributes } from '../../../common/api';
-import { CASE_USER_ACTION_SAVED_OBJECT, CASE_SAVED_OBJECT } from '../../saved_object_types';
+import {
+ CASE_USER_ACTION_SAVED_OBJECT,
+ CASE_SAVED_OBJECT,
+ SUB_CASE_SAVED_OBJECT,
+} from '../../saved_object_types';
import { ClientArgs } from '..';
interface GetCaseUserActionArgs extends ClientArgs {
caseId: string;
+ subCaseId?: string;
}
export interface UserActionItem {
@@ -41,18 +46,20 @@ export interface CaseUserActionServiceSetup {
export class CaseUserActionService {
constructor(private readonly log: Logger) {}
public setup = async (): Promise => ({
- getUserActions: async ({ client, caseId }: GetCaseUserActionArgs) => {
+ getUserActions: async ({ client, caseId, subCaseId }: GetCaseUserActionArgs) => {
try {
+ const id = subCaseId ?? caseId;
+ const type = subCaseId ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT;
const caseUserActionInfo = await client.find({
type: CASE_USER_ACTION_SAVED_OBJECT,
fields: [],
- hasReference: { type: CASE_SAVED_OBJECT, id: caseId },
+ hasReference: { type, id },
page: 1,
perPage: 1,
});
return await client.find({
type: CASE_USER_ACTION_SAVED_OBJECT,
- hasReference: { type: CASE_SAVED_OBJECT, id: caseId },
+ hasReference: { type, id },
page: 1,
perPage: caseUserActionInfo.total,
sortField: 'action_at',
diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts
index bc71df5d9e00..31b4cef1a9d4 100644
--- a/x-pack/plugins/security_solution/common/constants.ts
+++ b/x-pack/plugins/security_solution/common/constants.ts
@@ -171,7 +171,7 @@ export const ML_GROUP_IDS = [ML_GROUP_ID, LEGACY_ML_GROUP_ID];
/*
Rule notifications options
*/
-export const ENABLE_CASE_CONNECTOR = false;
+export const ENABLE_CASE_CONNECTOR = true;
export const NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS = [
'.email',
'.slack',
diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx
index c447e00cbb94..d02f7e0ee096 100644
--- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx
@@ -79,7 +79,12 @@ describe('AddComment ', () => {
await waitFor(() => {
expect(onCommentSaving).toBeCalled();
- expect(postComment).toBeCalledWith(addCommentProps.caseId, sampleData, onCommentPosted);
+ expect(postComment).toBeCalledWith({
+ caseId: addCommentProps.caseId,
+ data: sampleData,
+ subCaseId: undefined,
+ updateCase: onCommentPosted,
+ });
expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe('');
});
});
diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx
index 01b86a989e02..c94ef75523e2 100644
--- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx
@@ -39,11 +39,15 @@ interface AddCommentProps {
onCommentSaving?: () => void;
onCommentPosted: (newCase: Case) => void;
showLoading?: boolean;
+ subCaseId?: string;
}
export const AddComment = React.memo(
forwardRef(
- ({ caseId, disabled, showLoading = true, onCommentPosted, onCommentSaving }, ref) => {
+ (
+ { caseId, disabled, onCommentPosted, onCommentSaving, showLoading = true, subCaseId },
+ ref
+ ) => {
const { isLoading, postComment } = usePostComment();
const { form } = useForm({
@@ -80,10 +84,15 @@ export const AddComment = React.memo(
if (onCommentSaving != null) {
onCommentSaving();
}
- postComment(caseId, { ...data, type: CommentType.user }, onCommentPosted);
+ postComment({
+ caseId,
+ data: { ...data, type: CommentType.user },
+ updateCase: onCommentPosted,
+ subCaseId,
+ });
reset();
}
- }, [onCommentPosted, onCommentSaving, postComment, reset, submit, caseId]);
+ }, [caseId, onCommentPosted, onCommentSaving, postComment, reset, submit, subCaseId]);
return (
}>
);
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_show_alert.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_show_alert.test.tsx
index 5d619a39d0e7..789a6eb68e0f 100644
--- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_show_alert.test.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_show_alert.test.tsx
@@ -8,20 +8,24 @@
import React from 'react';
import { mount, ReactWrapper } from 'enzyme';
import { UserActionShowAlert } from './user_action_show_alert';
+import { RuleEcs } from '../../../../common/ecs/rule';
const props = {
id: 'action-id',
+ alertId: 'alert-id',
+ index: 'alert-index',
alert: {
_id: 'alert-id',
_index: 'alert-index',
- '@timestamp': '2021-01-07T13:58:31.487Z',
+ timestamp: '2021-01-07T13:58:31.487Z',
rule: {
- id: 'rule-id',
- name: 'Awesome Rule',
- from: '2021-01-07T13:58:31.487Z',
- to: '2021-01-07T14:58:31.487Z',
- },
+ id: ['rule-id'],
+ name: ['Awesome Rule'],
+ from: ['2021-01-07T13:58:31.487Z'],
+ to: ['2021-01-07T14:58:31.487Z'],
+ } as RuleEcs,
},
+ onShowAlertDetails: jest.fn(),
};
describe('UserActionShowAlert ', () => {
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_show_alert.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_show_alert.tsx
index ea4994d1c809..4f5ce0080641 100644
--- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_show_alert.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_show_alert.tsx
@@ -7,25 +7,24 @@
import React, { memo, useCallback } from 'react';
import { EuiToolTip, EuiButtonIcon } from '@elastic/eui';
-import deepEqual from 'fast-deep-equal';
-
-import { Alert } from '../case_view';
import * as i18n from './translations';
interface UserActionShowAlertProps {
id: string;
- alert: Alert;
+ alertId: string;
+ index: string;
onShowAlertDetails: (alertId: string, index: string) => void;
}
const UserActionShowAlertComponent = ({
id,
- alert,
+ alertId,
+ index,
onShowAlertDetails,
}: UserActionShowAlertProps) => {
- const onClick = useCallback(() => onShowAlertDetails(alert._id, alert._index), [
- alert._id,
- alert._index,
+ const onClick = useCallback(() => onShowAlertDetails(alertId, index), [
+ alertId,
+ index,
onShowAlertDetails,
]);
return (
@@ -41,10 +40,4 @@ const UserActionShowAlertComponent = ({
);
};
-export const UserActionShowAlert = memo(
- UserActionShowAlertComponent,
- (prevProps, nextProps) =>
- prevProps.id === nextProps.id &&
- deepEqual(prevProps.alert, nextProps.alert) &&
- prevProps.onShowAlertDetails === nextProps.onShowAlertDetails
-);
+export const UserActionShowAlert = memo(UserActionShowAlertComponent);
diff --git a/x-pack/plugins/security_solution/public/cases/containers/api.ts b/x-pack/plugins/security_solution/public/cases/containers/api.ts
index 00a45aadd2ae..c87e210b42bc 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/api.ts
+++ b/x-pack/plugins/security_solution/public/cases/containers/api.ts
@@ -5,6 +5,8 @@
* 2.0.
*/
+import { assign } from 'lodash';
+
import {
CasePatchRequest,
CasePostRequest,
@@ -16,6 +18,9 @@ import {
CaseUserActionsResponse,
CommentRequest,
CommentType,
+ SubCasePatchRequest,
+ SubCaseResponse,
+ SubCasesResponse,
User,
} from '../../../../case/common/api';
@@ -25,6 +30,8 @@ import {
CASE_STATUS_URL,
CASE_TAGS_URL,
CASES_URL,
+ SUB_CASE_DETAILS_URL,
+ SUB_CASES_PATCH_DEL_URL,
} from '../../../../case/common/constants';
import {
@@ -32,6 +39,8 @@ import {
getCasePushUrl,
getCaseDetailsUrl,
getCaseUserActionUrl,
+ getSubCaseDetailsUrl,
+ getSubCaseUserActionUrl,
} from '../../../../case/common/api/helpers';
import { KibanaServices } from '../../common/lib/kibana';
@@ -73,6 +82,34 @@ export const getCase = async (
return convertToCamelCase(decodeCaseResponse(response));
};
+export const getSubCase = async (
+ caseId: string,
+ subCaseId: string,
+ includeComments: boolean = true,
+ signal: AbortSignal
+): Promise => {
+ const [caseResponse, subCaseResponse] = await Promise.all([
+ KibanaServices.get().http.fetch(getCaseDetailsUrl(caseId), {
+ method: 'GET',
+ query: {
+ includeComments: false,
+ },
+ signal,
+ }),
+ KibanaServices.get().http.fetch(getSubCaseDetailsUrl(caseId, subCaseId), {
+ method: 'GET',
+ query: {
+ includeComments,
+ },
+ signal,
+ }),
+ ]);
+ const response = assign(caseResponse, subCaseResponse);
+ const subCaseIndex = response.subCaseIds?.findIndex((scId) => scId === response.id) ?? -1;
+ response.title = `${response.title}${subCaseIndex >= 0 ? ` ${subCaseIndex + 1}` : ''}`;
+ return convertToCamelCase(decodeCaseResponse(response));
+};
+
export const getCasesStatus = async (signal: AbortSignal): Promise => {
const response = await KibanaServices.get().http.fetch(CASE_STATUS_URL, {
method: 'GET',
@@ -111,6 +148,21 @@ export const getCaseUserActions = async (
return convertArrayToCamelCase(decodeCaseUserActionsResponse(response)) as CaseUserActions[];
};
+export const getSubCaseUserActions = async (
+ caseId: string,
+ subCaseId: string,
+ signal: AbortSignal
+): Promise => {
+ const response = await KibanaServices.get().http.fetch(
+ getSubCaseUserActionUrl(caseId, subCaseId),
+ {
+ method: 'GET',
+ signal,
+ }
+ );
+ return convertArrayToCamelCase(decodeCaseUserActionsResponse(response)) as CaseUserActions[];
+};
+
export const getCases = async ({
filterOptions = {
search: '',
@@ -167,6 +219,35 @@ export const patchCase = async (
return convertToCamelCase(decodeCasesResponse(response));
};
+export const patchSubCase = async (
+ caseId: string,
+ subCaseId: string,
+ updatedSubCase: Pick,
+ version: string,
+ signal: AbortSignal
+): Promise => {
+ const subCaseResponse = await KibanaServices.get().http.fetch(
+ SUB_CASE_DETAILS_URL,
+ {
+ method: 'PATCH',
+ body: JSON.stringify({ cases: [{ ...updatedSubCase, id: caseId, version }] }),
+ signal,
+ }
+ );
+ const caseResponse = await KibanaServices.get().http.fetch(
+ getCaseDetailsUrl(caseId),
+ {
+ method: 'GET',
+ query: {
+ includeComments: false,
+ },
+ signal,
+ }
+ );
+ const response = subCaseResponse.map((subCaseResp) => assign(caseResponse, subCaseResp));
+ return convertToCamelCase(decodeCasesResponse(response));
+};
+
export const patchCasesStatus = async (
cases: BulkUpdateStatus[],
signal: AbortSignal
@@ -182,13 +263,15 @@ export const patchCasesStatus = async (
export const postComment = async (
newComment: CommentRequest,
caseId: string,
- signal: AbortSignal
+ signal: AbortSignal,
+ subCaseId?: string
): Promise => {
const response = await KibanaServices.get().http.fetch(
`${CASES_URL}/${caseId}/comments`,
{
method: 'POST',
body: JSON.stringify(newComment),
+ ...(subCaseId ? { query: { subCaseId } } : {}),
signal,
}
);
@@ -200,7 +283,8 @@ export const patchComment = async (
commentId: string,
commentUpdate: string,
version: string,
- signal: AbortSignal
+ signal: AbortSignal,
+ subCaseId?: string
): Promise => {
const response = await KibanaServices.get().http.fetch(getCaseCommentsUrl(caseId), {
method: 'PATCH',
@@ -210,6 +294,7 @@ export const patchComment = async (
id: commentId,
version,
}),
+ ...(subCaseId ? { query: { subCaseId } } : {}),
signal,
});
return convertToCamelCase(decodeCaseResponse(response));
@@ -224,6 +309,15 @@ export const deleteCases = async (caseIds: string[], signal: AbortSignal): Promi
return response;
};
+export const deleteSubCases = async (caseIds: string[], signal: AbortSignal): Promise => {
+ const response = await KibanaServices.get().http.fetch(SUB_CASES_PATCH_DEL_URL, {
+ method: 'DELETE',
+ query: { ids: JSON.stringify(caseIds) },
+ signal,
+ });
+ return response;
+};
+
export const pushCase = async (
caseId: string,
connectorId: string,
diff --git a/x-pack/plugins/security_solution/public/cases/containers/mock.ts b/x-pack/plugins/security_solution/public/cases/containers/mock.ts
index 80d4816bedd5..d8692da986cb 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/mock.ts
+++ b/x-pack/plugins/security_solution/public/cases/containers/mock.ts
@@ -26,6 +26,7 @@ import { ConnectorTypes } from '../../../../case/common/api/connectors';
export { connectorsMock } from './configure/mock';
export const basicCaseId = 'basic-case-id';
+export const basicSubCaseId = 'basic-sub-case-id';
const basicCommentId = 'basic-comment-id';
const basicCreatedAt = '2020-02-19T23:06:33.798Z';
const basicUpdatedAt = '2020-02-20T15:02:57.995Z';
@@ -63,6 +64,10 @@ export const alertComment: Comment = {
createdBy: elasticUser,
pushedAt: null,
pushedBy: null,
+ rule: {
+ id: 'rule-id-1',
+ name: 'Awesome rule',
+ },
updatedAt: null,
updatedBy: null,
version: 'WzQ3LDFc',
@@ -95,6 +100,7 @@ export const basicCase: Case = {
settings: {
syncAlerts: true,
},
+ subCaseIds: [],
};
export const basicCasePost: Case = {
@@ -217,7 +223,7 @@ export const basicCaseSnake: CaseResponse = {
external_service: null,
updated_at: basicUpdatedAt,
updated_by: elasticUserSnake,
-};
+} as CaseResponse;
export const casesStatusSnake: CasesStatusResponse = {
count_closed_cases: 130,
diff --git a/x-pack/plugins/security_solution/public/cases/containers/types.ts b/x-pack/plugins/security_solution/public/cases/containers/types.ts
index 30ea83444346..d2931a790bd7 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/types.ts
+++ b/x-pack/plugins/security_solution/public/cases/containers/types.ts
@@ -18,7 +18,9 @@ import {
AssociationType,
} from '../../../../case/common/api';
-export { CaseConnector, ActionConnector } from '../../../../case/common/api';
+export { CaseConnector, ActionConnector, CaseStatuses } from '../../../../case/common/api';
+
+export type AllCaseType = AssociationType & CaseType;
export type Comment = CommentRequest & {
associationType: AssociationType;
@@ -52,26 +54,37 @@ export interface CaseExternalService {
externalTitle: string;
externalUrl: string;
}
-export interface Case {
+
+interface BasicCase {
id: string;
closedAt: string | null;
closedBy: ElasticUser | null;
comments: Comment[];
- connector: CaseConnector;
createdAt: string;
createdBy: ElasticUser;
- description: string;
- externalService: CaseExternalService | null;
status: CaseStatuses;
- tags: string[];
title: string;
totalAlerts: number;
totalComment: number;
- type: CaseType;
updatedAt: string | null;
updatedBy: ElasticUser | null;
version: string;
+}
+
+export interface SubCase extends BasicCase {
+ associationType: AssociationType;
+ caseParentId: string;
+}
+
+export interface Case extends BasicCase {
+ connector: CaseConnector;
+ description: string;
+ externalService: CaseExternalService | null;
+ subCases?: SubCase[] | null;
+ subCaseIds: string[];
settings: CaseAttributes['settings'];
+ tags: string[];
+ type: CaseType;
}
export interface QueryParams {
@@ -138,6 +151,7 @@ export interface ActionLicense {
export interface DeleteCase {
id: string;
title?: string;
+ type?: CaseType;
}
export interface FieldMappings {
@@ -153,7 +167,7 @@ export type UpdateKey = keyof Pick<
export interface UpdateByKey {
updateKey: UpdateKey;
updateValue: CasePatchRequest[UpdateKey];
- fetchCaseUserActions?: (caseId: string) => void;
+ fetchCaseUserActions?: (caseId: string, subCaseId?: string) => void;
updateCase?: (newCase: Case) => void;
caseData: Case;
onSuccess?: () => void;
diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_delete_cases.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_delete_cases.tsx
index b777b16b1c0c..923c20dcf8eb 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/use_delete_cases.tsx
+++ b/x-pack/plugins/security_solution/public/cases/containers/use_delete_cases.tsx
@@ -12,7 +12,7 @@ import {
useStateToaster,
} from '../../common/components/toasters';
import * as i18n from './translations';
-import { deleteCases } from './api';
+import { deleteCases, deleteSubCases } from './api';
import { DeleteCase } from './types';
interface DeleteState {
@@ -87,7 +87,13 @@ export const useDeleteCases = (): UseDeleteCase => {
try {
dispatch({ type: 'FETCH_INIT' });
const caseIds = cases.map((theCase) => theCase.id);
- await deleteCases(caseIds, abortCtrl.signal);
+ // We don't allow user batch delete sub cases on UI at the moment.
+ if (cases[0].type != null || cases.length > 1) {
+ await deleteCases(caseIds, abortCtrl.signal);
+ } else {
+ await deleteSubCases(caseIds, abortCtrl.signal);
+ }
+
if (!cancel) {
dispatch({ type: 'FETCH_SUCCESS', payload: true });
displaySuccessToast(
diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx
index 45827a4bebff..1c4476e3cb2b 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx
+++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx
@@ -5,13 +5,14 @@
* 2.0.
*/
-import { useEffect, useReducer, useCallback } from 'react';
+import { isEmpty } from 'lodash';
+import { useEffect, useReducer, useCallback, useRef } from 'react';
import { CaseStatuses, CaseType } from '../../../../case/common/api';
import { Case } from './types';
import * as i18n from './translations';
import { errorToToaster, useStateToaster } from '../../common/components/toasters';
-import { getCase } from './api';
+import { getCase, getSubCase } from './api';
import { getNoneConnector } from '../components/configure_cases/utils';
interface CaseState {
@@ -77,6 +78,7 @@ export const initialData: Case = {
updatedAt: null,
updatedBy: null,
version: '',
+ subCaseIds: [],
settings: {
syncAlerts: true,
},
@@ -87,31 +89,32 @@ export interface UseGetCase extends CaseState {
updateCase: (newCase: Case) => void;
}
-export const useGetCase = (caseId: string): UseGetCase => {
+export const useGetCase = (caseId: string, subCaseId?: string): UseGetCase => {
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: true,
isError: false,
data: initialData,
});
const [, dispatchToaster] = useStateToaster();
+ const abortCtrl = useRef(new AbortController());
+ const didCancel = useRef(false);
const updateCase = useCallback((newCase: Case) => {
dispatch({ type: 'UPDATE_CASE', payload: newCase });
}, []);
const callFetch = useCallback(async () => {
- let didCancel = false;
- const abortCtrl = new AbortController();
-
const fetchData = async () => {
dispatch({ type: 'FETCH_INIT' });
try {
- const response = await getCase(caseId, true, abortCtrl.signal);
- if (!didCancel) {
+ const response = await (subCaseId
+ ? getSubCase(caseId, subCaseId, true, abortCtrl.current.signal)
+ : getCase(caseId, true, abortCtrl.current.signal));
+ if (!didCancel.current) {
dispatch({ type: 'FETCH_SUCCESS', payload: response });
}
} catch (error) {
- if (!didCancel) {
+ if (!didCancel.current) {
errorToToaster({
title: i18n.ERROR_TITLE,
error: error.body && error.body.message ? new Error(error.body.message) : error,
@@ -121,17 +124,22 @@ export const useGetCase = (caseId: string): UseGetCase => {
}
}
};
+ didCancel.current = false;
+ abortCtrl.current.abort();
+ abortCtrl.current = new AbortController();
fetchData();
- return () => {
- didCancel = true;
- abortCtrl.abort();
- };
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [caseId]);
+ }, [caseId, subCaseId]);
useEffect(() => {
- callFetch();
+ if (!isEmpty(caseId)) {
+ callFetch();
+ }
+ return () => {
+ didCancel.current = true;
+ abortCtrl.current.abort();
+ };
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [caseId]);
+ }, [caseId, subCaseId]);
return { ...state, fetchCase: callFetch, updateCase };
};
diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.tsx
index 8ebd46e64296..12e5f6643351 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.tsx
+++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.tsx
@@ -6,12 +6,12 @@
*/
import { isEmpty, uniqBy } from 'lodash/fp';
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
import deepEqual from 'fast-deep-equal';
import { errorToToaster, useStateToaster } from '../../common/components/toasters';
import { CaseFullExternalService } from '../../../../case/common/api/cases';
-import { getCaseUserActions } from './api';
+import { getCaseUserActions, getSubCaseUserActions } from './api';
import * as i18n from './translations';
import { CaseConnector, CaseExternalService, CaseUserActions, ElasticUser } from './types';
import { convertToCamelCase, parseString } from './utils';
@@ -46,7 +46,7 @@ export const initialData: CaseUserActionsState = {
};
export interface UseGetCaseUserActions extends CaseUserActionsState {
- fetchCaseUserActions: (caseId: string) => void;
+ fetchCaseUserActions: (caseId: string, subCaseId?: string) => void;
}
const getExternalService = (value: string): CaseExternalService | null =>
@@ -238,26 +238,29 @@ export const getPushedInfo = (
export const useGetCaseUserActions = (
caseId: string,
- caseConnectorId: string
+ caseConnectorId: string,
+ subCaseId?: string
): UseGetCaseUserActions => {
const [caseUserActionsState, setCaseUserActionsState] = useState(
initialData
);
-
+ const abortCtrl = useRef(new AbortController());
+ const didCancel = useRef(false);
const [, dispatchToaster] = useStateToaster();
const fetchCaseUserActions = useCallback(
- (thisCaseId: string) => {
- let didCancel = false;
- const abortCtrl = new AbortController();
+ (thisCaseId: string, thisSubCaseId?: string) => {
const fetchData = async () => {
- setCaseUserActionsState({
- ...caseUserActionsState,
- isLoading: true,
- });
try {
- const response = await getCaseUserActions(thisCaseId, abortCtrl.signal);
- if (!didCancel) {
+ setCaseUserActionsState({
+ ...caseUserActionsState,
+ isLoading: true,
+ });
+
+ const response = await (thisSubCaseId
+ ? getSubCaseUserActions(thisCaseId, thisSubCaseId, abortCtrl.current.signal)
+ : getCaseUserActions(thisCaseId, abortCtrl.current.signal));
+ if (!didCancel.current) {
// Attention Future developer
// We are removing the first item because it will always be the creation of the case
// and we do not want it to simplify our life
@@ -265,7 +268,11 @@ export const useGetCaseUserActions = (
? uniqBy('actionBy.username', response).map((cau) => cau.actionBy)
: [];
- const caseUserActions = !isEmpty(response) ? response.slice(1) : [];
+ const caseUserActions = !isEmpty(response)
+ ? thisSubCaseId
+ ? response
+ : response.slice(1)
+ : [];
setCaseUserActionsState({
caseUserActions,
...getPushedInfo(caseUserActions, caseConnectorId),
@@ -275,7 +282,7 @@ export const useGetCaseUserActions = (
});
}
} catch (error) {
- if (!didCancel) {
+ if (!didCancel.current) {
errorToToaster({
title: i18n.ERROR_TITLE,
error: error.body && error.body.message ? new Error(error.body.message) : error,
@@ -292,21 +299,24 @@ export const useGetCaseUserActions = (
}
}
};
+ abortCtrl.current.abort();
+ abortCtrl.current = new AbortController();
fetchData();
- return () => {
- didCancel = true;
- abortCtrl.abort();
- };
},
// eslint-disable-next-line react-hooks/exhaustive-deps
- [caseUserActionsState, caseConnectorId]
+ [caseConnectorId]
);
useEffect(() => {
if (!isEmpty(caseId)) {
- fetchCaseUserActions(caseId);
+ fetchCaseUserActions(caseId, subCaseId);
}
+
+ return () => {
+ didCancel.current = true;
+ abortCtrl.current.abort();
+ };
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [caseId, caseConnectorId]);
+ }, [caseId, subCaseId]);
return { ...caseUserActionsState, fetchCaseUserActions };
};
diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx
index f9d4454f63ff..42cd0deafa04 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx
+++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx
@@ -9,7 +9,7 @@ import { renderHook, act } from '@testing-library/react-hooks';
import { CommentType } from '../../../../case/common/api';
import { usePostComment, UsePostComment } from './use_post_comment';
-import { basicCaseId } from './mock';
+import { basicCaseId, basicSubCaseId } from './mock';
import * as api from './api';
jest.mock('./api');
@@ -40,7 +40,7 @@ describe('usePostComment', () => {
});
});
- it('calls postComment with correct arguments', async () => {
+ it('calls postComment with correct arguments - case', async () => {
const spyOnPostCase = jest.spyOn(api, 'postComment');
await act(async () => {
@@ -49,9 +49,38 @@ describe('usePostComment', () => {
);
await waitForNextUpdate();
- result.current.postComment(basicCaseId, samplePost, updateCaseCallback);
+ result.current.postComment({
+ caseId: basicCaseId,
+ data: samplePost,
+ updateCase: updateCaseCallback,
+ });
await waitForNextUpdate();
- expect(spyOnPostCase).toBeCalledWith(samplePost, basicCaseId, abortCtrl.signal);
+ expect(spyOnPostCase).toBeCalledWith(samplePost, basicCaseId, abortCtrl.signal, undefined);
+ });
+ });
+
+ it('calls postComment with correct arguments - sub case', async () => {
+ const spyOnPostCase = jest.spyOn(api, 'postComment');
+
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(() =>
+ usePostComment()
+ );
+ await waitForNextUpdate();
+
+ result.current.postComment({
+ caseId: basicCaseId,
+ data: samplePost,
+ updateCase: updateCaseCallback,
+ subCaseId: basicSubCaseId,
+ });
+ await waitForNextUpdate();
+ expect(spyOnPostCase).toBeCalledWith(
+ samplePost,
+ basicCaseId,
+ abortCtrl.signal,
+ basicSubCaseId
+ );
});
});
@@ -61,7 +90,11 @@ describe('usePostComment', () => {
usePostComment()
);
await waitForNextUpdate();
- result.current.postComment(basicCaseId, samplePost, updateCaseCallback);
+ result.current.postComment({
+ caseId: basicCaseId,
+ data: samplePost,
+ updateCase: updateCaseCallback,
+ });
await waitForNextUpdate();
expect(result.current).toEqual({
isLoading: false,
@@ -77,7 +110,11 @@ describe('usePostComment', () => {
usePostComment()
);
await waitForNextUpdate();
- result.current.postComment(basicCaseId, samplePost, updateCaseCallback);
+ result.current.postComment({
+ caseId: basicCaseId,
+ data: samplePost,
+ updateCase: updateCaseCallback,
+ });
expect(result.current.isLoading).toBe(true);
});
@@ -94,7 +131,11 @@ describe('usePostComment', () => {
usePostComment()
);
await waitForNextUpdate();
- result.current.postComment(basicCaseId, samplePost, updateCaseCallback);
+ result.current.postComment({
+ caseId: basicCaseId,
+ data: samplePost,
+ updateCase: updateCaseCallback,
+ });
expect(result.current).toEqual({
isLoading: false,
diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx
index f2bd9d3f41f3..8fc8053c14f7 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx
+++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx
@@ -42,8 +42,14 @@ const dataFetchReducer = (state: NewCommentState, action: Action): NewCommentSta
}
};
+interface PostComment {
+ caseId: string;
+ data: CommentRequest;
+ updateCase?: (newCase: Case) => void;
+ subCaseId?: string;
+}
export interface UsePostComment extends NewCommentState {
- postComment: (caseId: string, data: CommentRequest, updateCase?: (newCase: Case) => void) => void;
+ postComment: (args: PostComment) => void;
}
export const usePostComment = (): UsePostComment => {
@@ -54,13 +60,13 @@ export const usePostComment = (): UsePostComment => {
const [, dispatchToaster] = useStateToaster();
const postMyComment = useCallback(
- async (caseId: string, data: CommentRequest, updateCase?: (newCase: Case) => void) => {
+ async ({ caseId, data, updateCase, subCaseId }: PostComment) => {
let cancel = false;
const abortCtrl = new AbortController();
try {
dispatch({ type: 'FETCH_INIT' });
- const response = await postComment(data, caseId, abortCtrl.signal);
+ const response = await postComment(data, caseId, abortCtrl.signal, subCaseId);
if (!cancel) {
dispatch({ type: 'FETCH_SUCCESS' });
if (updateCase) {
diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_update_case.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.test.tsx
index 62560244fe9c..0adf2cc0bf92 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/use_update_case.test.tsx
+++ b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.test.tsx
@@ -7,7 +7,7 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { useUpdateCase, UseUpdateCase } from './use_update_case';
-import { basicCase } from './mock';
+import { basicCase, basicSubCaseId } from './mock';
import * as api from './api';
import { UpdateKey } from './types';
@@ -84,7 +84,27 @@ describe('useUpdateCase', () => {
isError: false,
updateCaseProperty: result.current.updateCaseProperty,
});
- expect(fetchCaseUserActions).toBeCalledWith(basicCase.id);
+ expect(fetchCaseUserActions).toBeCalledWith(basicCase.id, undefined);
+ expect(updateCase).toBeCalledWith(basicCase);
+ expect(onSuccess).toHaveBeenCalled();
+ });
+ });
+
+ it('patch sub case', async () => {
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useUpdateCase({ caseId: basicCase.id, subCaseId: basicSubCaseId })
+ );
+ await waitForNextUpdate();
+ result.current.updateCaseProperty(sampleUpdate);
+ await waitForNextUpdate();
+ expect(result.current).toEqual({
+ updateKey: null,
+ isLoading: false,
+ isError: false,
+ updateCaseProperty: result.current.updateCaseProperty,
+ });
+ expect(fetchCaseUserActions).toBeCalledWith(basicCase.id, basicSubCaseId);
expect(updateCase).toBeCalledWith(basicCase);
expect(onSuccess).toHaveBeenCalled();
});
diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx
index b2b919ae1422..23a23caeb71b 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx
+++ b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx
@@ -5,12 +5,12 @@
* 2.0.
*/
-import { useReducer, useCallback } from 'react';
+import { useReducer, useCallback, useEffect, useRef } from 'react';
import { errorToToaster, useStateToaster } from '../../common/components/toasters';
-import { patchCase } from './api';
-import { UpdateKey, UpdateByKey } from './types';
+import { patchCase, patchSubCase } from './api';
+import { UpdateKey, UpdateByKey, CaseStatuses } from './types';
import * as i18n from './translations';
import { createUpdateSuccessToaster } from './utils';
@@ -57,13 +57,21 @@ const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState =>
export interface UseUpdateCase extends NewCaseState {
updateCaseProperty: (updates: UpdateByKey) => void;
}
-export const useUpdateCase = ({ caseId }: { caseId: string }): UseUpdateCase => {
+export const useUpdateCase = ({
+ caseId,
+ subCaseId,
+}: {
+ caseId: string;
+ subCaseId?: string;
+}): UseUpdateCase => {
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
updateKey: null,
});
const [, dispatchToaster] = useStateToaster();
+ const abortCtrl = useRef(new AbortController());
+ const didCancel = useRef(false);
const dispatchUpdateCaseProperty = useCallback(
async ({
@@ -75,20 +83,27 @@ export const useUpdateCase = ({ caseId }: { caseId: string }): UseUpdateCase =>
onSuccess,
onError,
}: UpdateByKey) => {
- let cancel = false;
- const abortCtrl = new AbortController();
-
try {
+ didCancel.current = false;
+ abortCtrl.current = new AbortController();
dispatch({ type: 'FETCH_INIT', payload: updateKey });
- const response = await patchCase(
- caseId,
- { [updateKey]: updateValue },
- caseData.version,
- abortCtrl.signal
- );
- if (!cancel) {
+ const response = await (updateKey === 'status' && subCaseId
+ ? patchSubCase(
+ caseId,
+ subCaseId,
+ { status: updateValue as CaseStatuses },
+ caseData.version,
+ abortCtrl.current.signal
+ )
+ : patchCase(
+ caseId,
+ { [updateKey]: updateValue },
+ caseData.version,
+ abortCtrl.current.signal
+ ));
+ if (!didCancel.current) {
if (fetchCaseUserActions != null) {
- fetchCaseUserActions(caseId);
+ fetchCaseUserActions(caseId, subCaseId);
}
if (updateCase != null) {
updateCase(response[0]);
@@ -104,26 +119,31 @@ export const useUpdateCase = ({ caseId }: { caseId: string }): UseUpdateCase =>
}
}
} catch (error) {
- if (!cancel) {
- errorToToaster({
- title: i18n.ERROR_TITLE,
- error: error.body && error.body.message ? new Error(error.body.message) : error,
- dispatchToaster,
- });
+ if (!didCancel.current) {
+ if (error.name !== 'AbortError') {
+ errorToToaster({
+ title: i18n.ERROR_TITLE,
+ error: error.body && error.body.message ? new Error(error.body.message) : error,
+ dispatchToaster,
+ });
+ }
dispatch({ type: 'FETCH_FAILURE' });
if (onError) {
onError();
}
}
}
- return () => {
- cancel = true;
- abortCtrl.abort();
- };
},
// eslint-disable-next-line react-hooks/exhaustive-deps
- []
+ [caseId, subCaseId]
);
+ useEffect(() => {
+ return () => {
+ didCancel.current = true;
+ abortCtrl.current.abort();
+ };
+ }, []);
+
return { ...state, updateCaseProperty: dispatchUpdateCaseProperty };
};
diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.test.tsx
index d7d98879459f..9ff266ad9c98 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.test.tsx
+++ b/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.test.tsx
@@ -7,7 +7,7 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { useUpdateComment, UseUpdateComment } from './use_update_comment';
-import { basicCase, basicCaseCommentPatch } from './mock';
+import { basicCase, basicCaseCommentPatch, basicSubCaseId } from './mock';
import * as api from './api';
jest.mock('./api');
@@ -43,7 +43,7 @@ describe('useUpdateComment', () => {
});
});
- it('calls patchComment with correct arguments', async () => {
+ it('calls patchComment with correct arguments - case', async () => {
const spyOnPatchComment = jest.spyOn(api, 'patchComment');
await act(async () => {
@@ -59,7 +59,30 @@ describe('useUpdateComment', () => {
basicCase.comments[0].id,
'updated comment',
basicCase.comments[0].version,
- abortCtrl.signal
+ abortCtrl.signal,
+ undefined
+ );
+ });
+ });
+
+ it('calls patchComment with correct arguments - sub case', async () => {
+ const spyOnPatchComment = jest.spyOn(api, 'patchComment');
+
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useUpdateComment()
+ );
+ await waitForNextUpdate();
+
+ result.current.patchComment({ ...sampleUpdate, subCaseId: basicSubCaseId });
+ await waitForNextUpdate();
+ expect(spyOnPatchComment).toBeCalledWith(
+ basicCase.id,
+ basicCase.comments[0].id,
+ 'updated comment',
+ basicCase.comments[0].version,
+ abortCtrl.signal,
+ basicSubCaseId
);
});
});
diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.tsx
index 6222d993bb79..e36b21823310 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.tsx
+++ b/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.tsx
@@ -57,6 +57,7 @@ interface UpdateComment {
commentId: string;
commentUpdate: string;
fetchUserActions: () => void;
+ subCaseId?: string;
updateCase: (newCase: Case) => void;
version: string;
}
@@ -78,6 +79,7 @@ export const useUpdateComment = (): UseUpdateComment => {
commentId,
commentUpdate,
fetchUserActions,
+ subCaseId,
updateCase,
version,
}: UpdateComment) => {
@@ -90,7 +92,8 @@ export const useUpdateComment = (): UseUpdateComment => {
commentId,
commentUpdate,
version,
- abortCtrl.signal
+ abortCtrl.signal,
+ subCaseId
);
if (!cancel) {
updateCase(response);
diff --git a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx
index 701ecdf8580f..edb84db89b87 100644
--- a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx
+++ b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx
@@ -21,7 +21,10 @@ import { savedObjectReadOnlyErrorMessage, CaseCallOut } from '../components/call
export const CaseDetailsPage = React.memo(() => {
const history = useHistory();
const userPermissions = useGetUserSavedObjectPermissions();
- const { detailName: caseId } = useParams<{ detailName?: string }>();
+ const { detailName: caseId, subCaseId } = useParams<{
+ detailName?: string;
+ subCaseId?: string;
+ }>();
const search = useGetUrlSearch(navTabs.case);
if (userPermissions != null && !userPermissions.read) {
@@ -38,7 +41,11 @@ export const CaseDetailsPage = React.memo(() => {
messages={[{ ...savedObjectReadOnlyErrorMessage }]}
/>
)}
-
+
>
diff --git a/x-pack/plugins/security_solution/public/cases/pages/index.tsx b/x-pack/plugins/security_solution/public/cases/pages/index.tsx
index 32c94e593665..314bdc9bfd11 100644
--- a/x-pack/plugins/security_solution/public/cases/pages/index.tsx
+++ b/x-pack/plugins/security_solution/public/cases/pages/index.tsx
@@ -15,7 +15,9 @@ import { ConfigureCasesPage } from './configure_cases';
const casesPagePath = '';
const caseDetailsPagePath = `${casesPagePath}/:detailName`;
-const caseDetailsPagePathWithCommentId = `${casesPagePath}/:detailName/:commentId`;
+const subCaseDetailsPagePath = `${caseDetailsPagePath}/sub-cases/:subCaseId`;
+const caseDetailsPagePathWithCommentId = `${caseDetailsPagePath}/:commentId`;
+const subCaseDetailsPagePathWithCommentId = `${subCaseDetailsPagePath}/:commentId`;
const createCasePagePath = `${casesPagePath}/create`;
const configureCasesPagePath = `${casesPagePath}/configure`;
@@ -27,7 +29,13 @@ const CaseContainerComponent: React.FC = () => (
-
+
+
+
+
+
+
+
diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_case.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_case.tsx
index 7a7da6f89306..82d8aac904e9 100644
--- a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_case.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_case.tsx
@@ -9,19 +9,43 @@ import { appendSearch } from './helpers';
export const getCaseUrl = (search?: string | null) => `${appendSearch(search ?? undefined)}`;
-export const getCaseDetailsUrl = ({ id, search }: { id: string; search?: string | null }) =>
- `/${encodeURIComponent(id)}${appendSearch(search ?? undefined)}`;
+export const getCaseDetailsUrl = ({
+ id,
+ search,
+ subCaseId,
+}: {
+ id: string;
+ search?: string | null;
+ subCaseId?: string;
+}) => {
+ if (subCaseId) {
+ return `/${encodeURIComponent(id)}/sub-cases/${encodeURIComponent(subCaseId)}${appendSearch(
+ search ?? undefined
+ )}`;
+ }
+ return `/${encodeURIComponent(id)}${appendSearch(search ?? undefined)}`;
+};
export const getCaseDetailsUrlWithCommentId = ({
id,
commentId,
search,
+ subCaseId,
}: {
id: string;
commentId: string;
search?: string | null;
-}) =>
- `/${encodeURIComponent(id)}/${encodeURIComponent(commentId)}${appendSearch(search ?? undefined)}`;
+ subCaseId?: string;
+}) => {
+ if (subCaseId) {
+ return `/${encodeURIComponent(id)}/sub-cases/${encodeURIComponent(
+ subCaseId
+ )}/${encodeURIComponent(commentId)}${appendSearch(search ?? undefined)}`;
+ }
+ return `/${encodeURIComponent(id)}/${encodeURIComponent(commentId)}${appendSearch(
+ search ?? undefined
+ )}`;
+};
export const getCreateCaseUrl = (search?: string | null) =>
`/create${appendSearch(search ?? undefined)}`;
diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.tsx
index 6b4148db2b1e..8e2f57a1a597 100644
--- a/x-pack/plugins/security_solution/public/common/components/links/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/links/index.tsx
@@ -164,24 +164,25 @@ export const NetworkDetailsLink = React.memo(NetworkDetailsLinkComponent);
const CaseDetailsLinkComponent: React.FC<{
children?: React.ReactNode;
detailName: string;
+ subCaseId?: string;
title?: string;
-}> = ({ children, detailName, title }) => {
+}> = ({ children, detailName, subCaseId, title }) => {
const { formatUrl, search } = useFormatUrl(SecurityPageName.case);
const { navigateToApp } = useKibana().services.application;
const goToCaseDetails = useCallback(
(ev) => {
ev.preventDefault();
navigateToApp(`${APP_ID}:${SecurityPageName.case}`, {
- path: getCaseDetailsUrl({ id: detailName, search }),
+ path: getCaseDetailsUrl({ id: detailName, search, subCaseId }),
});
},
- [detailName, navigateToApp, search]
+ [detailName, navigateToApp, search, subCaseId]
);
return (
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx
index 143c39daace6..7d577659d66e 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx
@@ -421,7 +421,7 @@ describe('alert actions', () => {
...mockEcsDataWithAlert,
timestamp: '2020-03-20T17:59:46.349Z',
};
- const result = determineToAndFrom({ ecsData: ecsDataMock });
+ const result = determineToAndFrom({ ecs: ecsDataMock });
expect(result.from).toEqual('2020-03-20T17:54:46.349Z');
expect(result.to).toEqual('2020-03-20T17:59:46.349Z');
@@ -431,7 +431,7 @@ describe('alert actions', () => {
const { timestamp, ...ecsDataMock } = {
...mockEcsDataWithAlert,
};
- const result = determineToAndFrom({ ecsData: ecsDataMock });
+ const result = determineToAndFrom({ ecs: ecsDataMock });
expect(result.from).toEqual('2020-03-01T17:54:46.349Z');
expect(result.to).toEqual('2020-03-01T17:59:46.349Z');
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx
index 9d38e2b369fa..14ccae250ac4 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx
@@ -39,6 +39,7 @@ import {
} from './helpers';
import { KueryFilterQueryKind } from '../../../common/store';
import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider';
+import { esFilters } from '../../../../../../../src/plugins/data/public';
export const getUpdateAlertsQuery = (eventIds: Readonly) => {
return {
@@ -102,17 +103,32 @@ export const updateAlertStatusAction = async ({
}
};
-export const determineToAndFrom = ({ ecsData }: { ecsData: Ecs }) => {
+export const determineToAndFrom = ({ ecs }: { ecs: Ecs[] | Ecs }) => {
+ if (Array.isArray(ecs)) {
+ const timestamps = ecs.reduce((acc, item) => {
+ if (item.timestamp != null) {
+ const dateTimestamp = new Date(item.timestamp);
+ if (!acc.includes(dateTimestamp.valueOf())) {
+ return [...acc, dateTimestamp.valueOf()];
+ }
+ }
+ return acc;
+ }, []);
+ return {
+ from: new Date(Math.min(...timestamps)).toISOString(),
+ to: new Date(Math.max(...timestamps)).toISOString(),
+ };
+ }
+ const ecsData = ecs as Ecs;
const ellapsedTimeRule = moment.duration(
moment().diff(
- dateMath.parse(ecsData.signal?.rule?.from != null ? ecsData.signal?.rule?.from[0] : 'now-0s')
+ dateMath.parse(ecsData?.signal?.rule?.from != null ? ecsData.signal?.rule?.from[0] : 'now-0s')
)
);
-
- const from = moment(ecsData.timestamp ?? new Date())
+ const from = moment(ecsData?.timestamp ?? new Date())
.subtract(ellapsedTimeRule)
.toISOString();
- const to = moment(ecsData.timestamp ?? new Date()).toISOString();
+ const to = moment(ecsData?.timestamp ?? new Date()).toISOString();
return { to, from };
};
@@ -128,37 +144,41 @@ const getFiltersFromRule = (filters: string[]): Filter[] =>
}, [] as Filter[]);
export const getThresholdAggregationDataProvider = (
- ecsData: Ecs,
+ ecsData: Ecs | Ecs[],
nonEcsData: TimelineNonEcsData[]
): DataProvider[] => {
- const aggregationField = ecsData.signal?.rule?.threshold?.field!;
- const aggregationValue =
- get(aggregationField, ecsData) ?? find(['field', aggregationField], nonEcsData)?.value;
- const dataProviderValue = Array.isArray(aggregationValue)
- ? aggregationValue[0]
- : aggregationValue;
+ const thresholdEcsData: Ecs[] = Array.isArray(ecsData) ? ecsData : [ecsData];
+ return thresholdEcsData.reduce((acc, tresholdData) => {
+ const aggregationField = tresholdData.signal?.rule?.threshold?.field!;
+ const aggregationValue =
+ get(aggregationField, tresholdData) ?? find(['field', aggregationField], nonEcsData)?.value;
+ const dataProviderValue = Array.isArray(aggregationValue)
+ ? aggregationValue[0]
+ : aggregationValue;
- if (!dataProviderValue) {
- return [];
- }
+ if (!dataProviderValue) {
+ return acc;
+ }
- const aggregationFieldId = aggregationField.replace('.', '-');
+ const aggregationFieldId = aggregationField.replace('.', '-');
- return [
- {
- and: [],
- id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-${aggregationFieldId}-${dataProviderValue}`,
- name: aggregationField,
- enabled: true,
- excluded: false,
- kqlQuery: '',
- queryMatch: {
- field: aggregationField,
- value: dataProviderValue,
- operator: ':',
+ return [
+ ...acc,
+ {
+ and: [],
+ id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-${aggregationFieldId}-${dataProviderValue}`,
+ name: aggregationField,
+ enabled: true,
+ excluded: false,
+ kqlQuery: '',
+ queryMatch: {
+ field: aggregationField,
+ value: dataProviderValue,
+ operator: ':',
+ },
},
- },
- ];
+ ];
+ }, []);
};
export const isEqlRuleWithGroupId = (ecsData: Ecs) =>
@@ -169,20 +189,134 @@ export const isEqlRuleWithGroupId = (ecsData: Ecs) =>
export const isThresholdRule = (ecsData: Ecs) =>
ecsData.signal?.rule?.type?.length && ecsData.signal?.rule?.type[0] === 'threshold';
+export const buildAlertsKqlFilter = (
+ key: '_id' | 'signal.group.id',
+ alertIds: string[]
+): Filter[] => {
+ return [
+ {
+ query: {
+ bool: {
+ filter: {
+ ids: {
+ values: alertIds,
+ },
+ },
+ },
+ },
+ meta: {
+ alias: 'Alert Ids',
+ negate: false,
+ disabled: false,
+ type: 'phrases',
+ key,
+ value: alertIds.join(),
+ params: alertIds,
+ },
+ $state: {
+ store: esFilters.FilterStateStore.APP_STATE,
+ },
+ },
+ ];
+};
+
+export const buildTimelineDataProviderOrFilter = (
+ alertsIds: string[],
+ _id: string
+): { filters: Filter[]; dataProviders: DataProvider[] } => {
+ if (!isEmpty(alertsIds)) {
+ return {
+ dataProviders: [],
+ filters: buildAlertsKqlFilter('_id', alertsIds),
+ };
+ }
+ return {
+ filters: [],
+ dataProviders: [
+ {
+ and: [],
+ id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-alert-id-${_id}`,
+ name: _id,
+ enabled: true,
+ excluded: false,
+ kqlQuery: '',
+ queryMatch: {
+ field: '_id',
+ value: _id,
+ operator: ':' as const,
+ },
+ },
+ ],
+ };
+};
+
+export const buildEqlDataProviderOrFilter = (
+ alertsIds: string[],
+ ecs: Ecs[] | Ecs
+): { filters: Filter[]; dataProviders: DataProvider[] } => {
+ if (!isEmpty(alertsIds) && Array.isArray(ecs)) {
+ return {
+ dataProviders: [],
+ filters: buildAlertsKqlFilter(
+ 'signal.group.id',
+ ecs.reduce((acc, ecsData) => {
+ const signalGroupId = ecsData.signal?.group?.id?.length
+ ? ecsData.signal?.group?.id[0]
+ : 'unknown-signal-group-id';
+ if (!acc.includes(signalGroupId)) {
+ return [...acc, signalGroupId];
+ }
+ return acc;
+ }, [])
+ ),
+ };
+ } else if (!Array.isArray(ecs)) {
+ const signalGroupId = ecs.signal?.group?.id?.length
+ ? ecs.signal?.group?.id[0]
+ : 'unknown-signal-group-id';
+ return {
+ dataProviders: [
+ {
+ and: [],
+ id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-alert-id-${signalGroupId}`,
+ name: ecs._id,
+ enabled: true,
+ excluded: false,
+ kqlQuery: '',
+ queryMatch: {
+ field: 'signal.group.id',
+ value: signalGroupId,
+ operator: ':' as const,
+ },
+ },
+ ],
+ filters: [],
+ };
+ }
+ return { filters: [], dataProviders: [] };
+};
+
export const sendAlertToTimelineAction = async ({
apolloClient,
createTimeline,
- ecsData,
+ ecsData: ecs,
nonEcsData,
updateTimelineIsLoading,
searchStrategyClient,
}: SendAlertToTimelineActionProps) => {
+ /* FUTURE DEVELOPER
+ * We are making an assumption here that if you have an array of ecs data they are all coming from the same rule
+ * but we still want to determine the filter for each alerts
+ */
+ const ecsData: Ecs = Array.isArray(ecs) && ecs.length > 0 ? ecs[0] : (ecs as Ecs);
+ const alertIds = Array.isArray(ecs) ? ecs.map((d) => d._id) : [];
const noteContent = ecsData.signal?.rule?.note != null ? ecsData.signal?.rule?.note[0] : '';
const timelineId =
ecsData.signal?.rule?.timeline_id != null ? ecsData.signal?.rule?.timeline_id[0] : '';
- const { to, from } = determineToAndFrom({ ecsData });
+ const { to, from } = determineToAndFrom({ ecs });
- if (!isEmpty(timelineId) && apolloClient != null) {
+ // For now we do not want to populate the template timeline if we have alertIds
+ if (!isEmpty(timelineId) && apolloClient != null && isEmpty(alertIds)) {
try {
updateTimelineIsLoading({ id: TimelineId.active, isLoading: true });
const [responseTimeline, eventDataResp] = await Promise.all([
@@ -275,7 +409,7 @@ export const sendAlertToTimelineAction = async ({
...timelineDefaults,
description: `_id: ${ecsData._id}`,
filters: getFiltersFromRule(ecsData.signal?.rule?.filters as string[]),
- dataProviders: [...getThresholdAggregationDataProvider(ecsData, nonEcsData)],
+ dataProviders: [...getThresholdAggregationDataProvider(ecs, nonEcsData)],
id: TimelineId.active,
indexNames: [],
dateRange: {
@@ -301,36 +435,11 @@ export const sendAlertToTimelineAction = async ({
ruleNote: noteContent,
});
} else {
- let dataProviders = [
- {
- and: [],
- id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-alert-id-${ecsData._id}`,
- name: ecsData._id,
- enabled: true,
- excluded: false,
- kqlQuery: '',
- queryMatch: {
- field: '_id',
- value: ecsData._id,
- operator: ':' as const,
- },
- },
- ];
+ let { dataProviders, filters } = buildTimelineDataProviderOrFilter(alertIds ?? [], ecsData._id);
if (isEqlRuleWithGroupId(ecsData)) {
- const signalGroupId = ecsData.signal?.group?.id?.length
- ? ecsData.signal?.group?.id[0]
- : 'unknown-signal-group-id';
- dataProviders = [
- {
- ...dataProviders[0],
- id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-alert-id-${signalGroupId}`,
- queryMatch: {
- field: 'signal.group.id',
- value: signalGroupId,
- operator: ':' as const,
- },
- },
- ];
+ const tempEql = buildEqlDataProviderOrFilter(alertIds ?? [], ecs);
+ dataProviders = tempEql.dataProviders;
+ filters = tempEql.filters;
}
return createTimeline({
@@ -346,6 +455,7 @@ export const sendAlertToTimelineAction = async ({
end: to,
},
eventType: 'all',
+ filters,
kqlQuery: {
filterQuery: {
kuery: {
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx
index d6813fdef8e5..2f0fee980c21 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx
@@ -24,14 +24,18 @@ import {
} from '../translations';
interface InvestigateInTimelineActionProps {
- ariaLabel?: string;
- ecsRowData: Ecs;
+ ecsRowData: Ecs | Ecs[] | null;
nonEcsRowData: TimelineNonEcsData[];
+ ariaLabel?: string;
+ alertIds?: string[];
+ fetchEcsAlertsData?: (alertIds?: string[]) => Promise;
}
const InvestigateInTimelineActionComponent: React.FC = ({
ariaLabel = ACTION_INVESTIGATE_IN_TIMELINE_ARIA_LABEL,
+ alertIds,
ecsRowData,
+ fetchEcsAlertsData,
nonEcsRowData,
}) => {
const {
@@ -66,25 +70,42 @@ const InvestigateInTimelineActionComponent: React.FC
- sendAlertToTimelineAction({
- apolloClient,
- createTimeline,
- ecsData: ecsRowData,
- nonEcsData: nonEcsRowData,
- searchStrategyClient,
- updateTimelineIsLoading,
- }),
- [
- apolloClient,
- createTimeline,
- ecsRowData,
- nonEcsRowData,
- searchStrategyClient,
- updateTimelineIsLoading,
- ]
- );
+ const investigateInTimelineAlertClick = useCallback(async () => {
+ try {
+ if (ecsRowData != null) {
+ await sendAlertToTimelineAction({
+ apolloClient,
+ createTimeline,
+ ecsData: ecsRowData,
+ nonEcsData: nonEcsRowData,
+ searchStrategyClient,
+ updateTimelineIsLoading,
+ });
+ }
+ if (ecsRowData == null && fetchEcsAlertsData) {
+ const alertsEcsData = await fetchEcsAlertsData(alertIds);
+ await sendAlertToTimelineAction({
+ apolloClient,
+ createTimeline,
+ ecsData: alertsEcsData,
+ nonEcsData: nonEcsRowData,
+ searchStrategyClient,
+ updateTimelineIsLoading,
+ });
+ }
+ } catch {
+ // TODO show a toaster that something went wrong
+ }
+ }, [
+ alertIds,
+ apolloClient,
+ createTimeline,
+ ecsRowData,
+ fetchEcsAlertsData,
+ nonEcsRowData,
+ searchStrategyClient,
+ updateTimelineIsLoading,
+ ]);
return (
;
createTimeline: CreateTimeline;
- ecsData: Ecs;
+ ecsData: Ecs | Ecs[];
nonEcsData: TimelineNonEcsData[];
updateTimelineIsLoading: UpdateTimelineLoading;
searchStrategyClient: ISearchStart;
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx
index 475160b78727..8557e1082c1c 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx
@@ -5,6 +5,7 @@
* 2.0.
*/
+import { isEmpty } from 'lodash';
import React, { SetStateAction, useEffect, useState } from 'react';
import { fetchQueryAlerts } from './api';
@@ -80,7 +81,9 @@ export const useQueryAlerts = (
}
};
- fetchData();
+ if (!isEmpty(query)) {
+ fetchData();
+ }
return () => {
isSubscribed = false;
abortCtrl.abort();
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts
index e33ee4d5762a..15261ab5fad0 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts
@@ -172,8 +172,10 @@ export const singleBulkCreate = async ({
logger.debug(buildRuleMessage(`took property says bulk took: ${response.took} milliseconds`));
const createdItems = filteredEvents.hits.hits
- .map((doc) =>
- buildBulkBody({
+ .map((doc, index) => ({
+ _id: response.items[index].create?._id ?? '',
+ _index: response.items[index].create?._index ?? '',
+ ...buildBulkBody({
doc,
ruleParams,
id,
@@ -187,8 +189,8 @@ export const singleBulkCreate = async ({
enabled,
tags,
throttle,
- })
- )
+ }),
+ }))
.filter((_, index) => get(response.items[index], 'create.status') === 201);
const createdItemsCount = createdItems.length;
const duplicateSignalsCount = countBy(response.items, 'create.status')['409'];
@@ -263,7 +265,11 @@ export const bulkInsertSignals = async (
const createdItemsCount = countBy(response.items, 'create.status')['201'] ?? 0;
const createdItems = signals
- .map((doc) => doc._source)
+ .map((doc, index) => ({
+ ...doc._source,
+ _id: response.items[index].create?._id ?? '',
+ _index: response.items[index].create?._index ?? '',
+ }))
.filter((_, index) => get(response.items[index], 'create.status') === 201);
logger.debug(`bulk created ${createdItemsCount} signals`);
return { bulkCreateDuration: makeFloatString(end - start), createdItems, createdItemsCount };
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 344a07e53e3e..a5d4b5d991b4 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -17250,7 +17250,6 @@
"xpack.securitySolution.case.caseView.reporterLabel": "報告者",
"xpack.securitySolution.case.caseView.requiredUpdateToExternalService": "{ externalService }インシデントの更新が必要です",
"xpack.securitySolution.case.caseView.sendEmalLinkAria": "クリックすると、{user}に電子メールを送信します",
- "xpack.securitySolution.case.caseView.showAlertDeletedTooltip": "アラートが見つかりません",
"xpack.securitySolution.case.caseView.showAlertTooltip": "アラートの詳細を表示",
"xpack.securitySolution.case.caseView.statusLabel": "ステータス",
"xpack.securitySolution.case.caseView.tags": "タグ",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 579a06d44e65..f84b993fe563 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -17293,7 +17293,6 @@
"xpack.securitySolution.case.caseView.reporterLabel": "报告者",
"xpack.securitySolution.case.caseView.requiredUpdateToExternalService": "需要更新 { externalService } 事件",
"xpack.securitySolution.case.caseView.sendEmalLinkAria": "单击可向 {user} 发送电子邮件",
- "xpack.securitySolution.case.caseView.showAlertDeletedTooltip": "未找到告警",
"xpack.securitySolution.case.caseView.showAlertTooltip": "显示告警详情",
"xpack.securitySolution.case.caseView.statusLabel": "状态",
"xpack.securitySolution.case.caseView.tags": "标签",
diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts
index 2250b481c372..86b1c3031cbe 100644
--- a/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts
+++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts
@@ -95,6 +95,10 @@ export default ({ getService }: FtrProviderContext): void => {
type: CommentType.alert,
alertId: 'test-id',
index: 'test-index',
+ rule: {
+ id: 'id',
+ name: 'name',
+ },
})
.expect(400);
});
@@ -110,6 +114,10 @@ export default ({ getService }: FtrProviderContext): void => {
type: CommentType.generatedAlert,
alerts: [{ _id: 'id1' }],
index: 'test-index',
+ rule: {
+ id: 'id',
+ name: 'name',
+ },
})
.expect(400);
});
@@ -167,6 +175,10 @@ export default ({ getService }: FtrProviderContext): void => {
type: CommentType.alert,
alertId: 'new-id',
index: postCommentAlertReq.index,
+ rule: {
+ id: 'id',
+ name: 'name',
+ },
})
.expect(200);
@@ -230,6 +242,10 @@ export default ({ getService }: FtrProviderContext): void => {
type: CommentType.alert,
alertId: 'test-id',
index: 'test-index',
+ rule: {
+ id: 'id',
+ name: 'name',
+ },
})
.expect(400);
});
@@ -302,6 +318,10 @@ export default ({ getService }: FtrProviderContext): void => {
type: CommentType.alert,
index: 'test-index',
alertId: 'test-id',
+ rule: {
+ id: 'id',
+ name: 'name',
+ },
};
for (const attribute of ['alertId', 'index']) {
@@ -341,6 +361,10 @@ export default ({ getService }: FtrProviderContext): void => {
type: CommentType.alert,
index: 'test-index',
alertId: 'test-id',
+ rule: {
+ id: 'id',
+ name: 'name',
+ },
[attribute]: attribute,
})
.expect(400);
diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts
index 1ce011985d9e..fb095c117cdf 100644
--- a/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts
+++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts
@@ -148,6 +148,10 @@ export default ({ getService }: FtrProviderContext): void => {
type: CommentType.alert,
index: 'test-index',
alertId: 'test-id',
+ rule: {
+ id: 'id',
+ name: 'name',
+ },
};
for (const attribute of ['alertId', 'index']) {
@@ -176,6 +180,10 @@ export default ({ getService }: FtrProviderContext): void => {
[attribute]: attribute,
alertId: 'test-id',
index: 'test-index',
+ rule: {
+ id: 'id',
+ name: 'name',
+ },
})
.expect(400);
}
@@ -296,6 +304,10 @@ export default ({ getService }: FtrProviderContext): void => {
.send({
alertId: alert._id,
index: alert._index,
+ rule: {
+ id: 'id',
+ name: 'name',
+ },
type: CommentType.alert,
})
.expect(200);
@@ -346,6 +358,10 @@ export default ({ getService }: FtrProviderContext): void => {
.send({
alertId: alert._id,
index: alert._index,
+ rule: {
+ id: 'id',
+ name: 'name',
+ },
type: CommentType.alert,
})
.expect(200);
diff --git a/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts
index dcc49152e4db..43d6be196da0 100644
--- a/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts
+++ b/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts
@@ -402,6 +402,10 @@ export default ({ getService }: FtrProviderContext): void => {
.send({
alertId: alert._id,
index: alert._index,
+ rule: {
+ id: 'id',
+ name: 'name',
+ },
type: CommentType.alert,
})
.expect(200);
@@ -453,6 +457,10 @@ export default ({ getService }: FtrProviderContext): void => {
alertId: alert._id,
index: alert._index,
type: CommentType.alert,
+ rule: {
+ id: 'id',
+ name: 'name',
+ },
})
.expect(200);
@@ -503,6 +511,10 @@ export default ({ getService }: FtrProviderContext): void => {
.send({
alertId: alert._id,
index: alert._index,
+ rule: {
+ id: 'id',
+ name: 'name',
+ },
type: CommentType.alert,
})
.expect(200);
@@ -570,6 +582,10 @@ export default ({ getService }: FtrProviderContext): void => {
alertId: alert._id,
index: alert._index,
type: CommentType.alert,
+ rule: {
+ id: 'id',
+ name: 'name',
+ },
})
.expect(200);
diff --git a/x-pack/test/case_api_integration/basic/tests/connectors/case.ts b/x-pack/test/case_api_integration/basic/tests/connectors/case.ts
index 01dd6ed5404c..4812ead2c4c7 100644
--- a/x-pack/test/case_api_integration/basic/tests/connectors/case.ts
+++ b/x-pack/test/case_api_integration/basic/tests/connectors/case.ts
@@ -736,7 +736,12 @@ export default ({ getService }: FtrProviderContext): void => {
subAction: 'addComment',
subActionParams: {
caseId: caseRes.body.id,
- comment: { alertId: alert._id, index: alert._index, type: CommentType.alert },
+ comment: {
+ alertId: alert._id,
+ index: alert._index,
+ type: CommentType.alert,
+ rule: { id: 'id', name: 'name' },
+ },
},
};
@@ -784,7 +789,12 @@ export default ({ getService }: FtrProviderContext): void => {
.expect(200);
createdActionId = createdAction.id;
- const comment = { alertId: 'test-id', index: 'test-index', type: CommentType.alert };
+ const comment = {
+ alertId: 'test-id',
+ index: 'test-index',
+ type: CommentType.alert,
+ rule: { id: 'id', name: 'name' },
+ };
const params = {
subAction: 'addComment',
subActionParams: {
@@ -876,7 +886,12 @@ export default ({ getService }: FtrProviderContext): void => {
subAction: 'addComment',
subActionParams: {
caseId: '123',
- comment: { alertId: 'test-id', index: 'test-index', type: CommentType.alert },
+ comment: {
+ alertId: 'test-id',
+ index: 'test-index',
+ type: CommentType.alert,
+ rule: { id: 'id', name: 'name' },
+ },
},
};
@@ -1017,7 +1032,12 @@ export default ({ getService }: FtrProviderContext): void => {
subAction: 'addComment',
subActionParams: {
caseId: caseRes.body.id,
- comment: { alertId: 'test-id', index: 'test-index', type: CommentType.alert },
+ comment: {
+ alertId: 'test-id',
+ index: 'test-index',
+ type: CommentType.alert,
+ rule: { id: 'id', name: 'name' },
+ },
},
};
diff --git a/x-pack/test/case_api_integration/common/lib/mock.ts b/x-pack/test/case_api_integration/common/lib/mock.ts
index 2f4fa1b30f56..f6fd2b1a6b3b 100644
--- a/x-pack/test/case_api_integration/common/lib/mock.ts
+++ b/x-pack/test/case_api_integration/common/lib/mock.ts
@@ -8,6 +8,7 @@
import {
CommentSchemaType,
ContextTypeGeneratedAlertType,
+ createAlertsString,
isCommentGeneratedAlert,
transformConnectorComment,
} from '../../../../plugins/case/server/connectors';
@@ -70,12 +71,15 @@ export const postCommentUserReq: CommentRequestUserType = {
export const postCommentAlertReq: CommentRequestAlertType = {
alertId: 'test-id',
index: 'test-index',
+ rule: { id: 'test-rule-id', name: 'test-index-id' },
type: CommentType.alert,
};
export const postCommentGenAlertReq: ContextTypeGeneratedAlertType = {
- alerts: [{ _id: 'test-id' }, { _id: 'test-id2' }],
- index: 'test-index',
+ alerts: createAlertsString([
+ { _id: 'test-id', _index: 'test-index', ruleId: 'rule-id', ruleName: 'rule name' },
+ { _id: 'test-id2', _index: 'test-index', ruleId: 'rule-id', ruleName: 'rule name' },
+ ]),
type: CommentType.generatedAlert,
};