mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Cases] Delete users actions and add audit log (#145632)
This PR deletes users actions when the case is deleted. It makes a few improvements: - Uses point in time finder for the different case entities when deleting instead of requesting a single page of 10k results - Refactors the User actions class to receive the `unsecuredSavedObjectClient` as a parameter when initializing the class - Adds the audit log message when a user action is created - Leverages the bulk delete method from the saved object core library Fixes: https://github.com/elastic/kibana/issues/143657 https://github.com/elastic/kibana/issues/145124 <details><summary>User actions removed when deleting cases</summary> https://user-images.githubusercontent.com/56361221/204568129-525e176c-84af-4197-8f80-fc0801676226.mov </details> ## Example Audit Log Messages ``` [2022-11-29T10:10:32.365-05:00][INFO ][plugins.security.audit.ecs] User created case id: f7138410-6ff7-11ed-860b-4fd7293e59c1 - user action id: f73256b0-6ff7-11ed-860b-4fd7293e59c1 [2022-11-29T10:11:08.716-05:00][INFO ][plugins.security.audit.ecs] User assigned uids: [u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0] to case id: f7138410-6ff7-11ed-860b-4fd7293e59c1 - user action id: 0cf9e670-6ff8-11ed-860b-4fd7293e59c1 ``` ## Testing To view the audit log message add these fields to your `kibana.dev.yml` file ``` xpack.security.audit.enabled: true xpack.security.audit.appender.type: 'console' xpack.security.audit.appender.layout.type: 'pattern' xpack.security.audit.appender.layout.highlight: true ``` Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
8f71351a94
commit
f0777b3bc1
38 changed files with 2691 additions and 1042 deletions
|
@ -34,9 +34,6 @@ export const Actions = {
|
|||
push_to_service: 'push_to_service',
|
||||
} as const;
|
||||
|
||||
export type ActionOperationKeys = keyof typeof Actions;
|
||||
export type ActionOperationValues = typeof Actions[ActionOperationKeys];
|
||||
|
||||
/* To the next developer, if you add/removed fields here
|
||||
* make sure to check this file (x-pack/plugins/cases/server/services/user_actions/helpers.ts) too
|
||||
*/
|
||||
|
|
|
@ -87,7 +87,6 @@ export async function deleteAll(
|
|||
});
|
||||
|
||||
await userActionService.bulkCreateAttachmentDeletion({
|
||||
unsecuredSavedObjectsClient,
|
||||
caseId: caseID,
|
||||
attachments: comments.saved_objects.map((comment) => ({
|
||||
id: comment.id,
|
||||
|
@ -154,7 +153,6 @@ export async function deleteComment(
|
|||
await userActionService.createUserAction({
|
||||
type: ActionTypes.comment,
|
||||
action: Actions.delete,
|
||||
unsecuredSavedObjectsClient,
|
||||
caseId: id,
|
||||
attachmentId: attachmentID,
|
||||
payload: { attachment: { ...myComment.attributes } },
|
||||
|
|
|
@ -40,7 +40,6 @@ export const create = async (
|
|||
clientArgs: CasesClientArgs
|
||||
): Promise<CaseResponse> => {
|
||||
const {
|
||||
unsecuredSavedObjectsClient,
|
||||
services: { caseService, userActionService, licensingService, notificationService },
|
||||
user,
|
||||
logger,
|
||||
|
@ -105,7 +104,6 @@ export const create = async (
|
|||
|
||||
await userActionService.createUserAction({
|
||||
type: ActionTypes.create_case,
|
||||
unsecuredSavedObjectsClient,
|
||||
caseId: newCase.id,
|
||||
user,
|
||||
payload: {
|
||||
|
|
|
@ -5,11 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import pMap from 'p-map';
|
||||
import { Boom } from '@hapi/boom';
|
||||
import type { SavedObjectsFindResponse } from '@kbn/core/server';
|
||||
import type { CommentAttributes } from '../../../common/api';
|
||||
import { MAX_CONCURRENT_SEARCHES } from '../../../common/constants';
|
||||
import type { SavedObjectsBulkDeleteObject } from '@kbn/core/server';
|
||||
import {
|
||||
CASE_COMMENT_SAVED_OBJECT,
|
||||
CASE_SAVED_OBJECT,
|
||||
CASE_USER_ACTION_SAVED_OBJECT,
|
||||
} from '../../../common/constants';
|
||||
import type { CasesClientArgs } from '..';
|
||||
import { createCaseError } from '../../common/error';
|
||||
import type { OwnerEntity } from '../../authorization';
|
||||
|
@ -23,7 +25,6 @@ import { Operations } from '../../authorization';
|
|||
export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): Promise<void> {
|
||||
const {
|
||||
unsecuredSavedObjectsClient,
|
||||
user,
|
||||
services: { caseService, attachmentService, userActionService },
|
||||
logger,
|
||||
authorization,
|
||||
|
@ -49,56 +50,27 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P
|
|||
entities: Array.from(entities.values()),
|
||||
});
|
||||
|
||||
const deleteCasesMapper = async (id: string) =>
|
||||
caseService.deleteCase({
|
||||
id,
|
||||
refresh: false,
|
||||
});
|
||||
|
||||
// Ensuring we don't too many concurrent deletions running.
|
||||
await pMap(ids, deleteCasesMapper, {
|
||||
concurrency: MAX_CONCURRENT_SEARCHES,
|
||||
});
|
||||
|
||||
const getCommentsMapper = async (id: string) =>
|
||||
caseService.getAllCaseComments({
|
||||
id,
|
||||
});
|
||||
|
||||
// Ensuring we don't too many concurrent get running.
|
||||
const comments = await pMap(ids, getCommentsMapper, {
|
||||
concurrency: MAX_CONCURRENT_SEARCHES,
|
||||
});
|
||||
|
||||
/**
|
||||
* This is a nested pMap.Mapper.
|
||||
* Each element of the comments array contains all comments of a particular case.
|
||||
* For that reason we need first to create a map that iterate over all cases
|
||||
* and return a pMap that deletes the comments for that case
|
||||
*/
|
||||
const deleteCommentsMapper = async (commentRes: SavedObjectsFindResponse<CommentAttributes>) =>
|
||||
pMap(commentRes.saved_objects, (comment) =>
|
||||
attachmentService.delete({
|
||||
unsecuredSavedObjectsClient,
|
||||
attachmentId: comment.id,
|
||||
refresh: false,
|
||||
})
|
||||
);
|
||||
|
||||
// Ensuring we don't too many concurrent deletions running.
|
||||
await pMap(comments, deleteCommentsMapper, {
|
||||
concurrency: MAX_CONCURRENT_SEARCHES,
|
||||
});
|
||||
|
||||
await userActionService.bulkCreateCaseDeletion({
|
||||
const attachmentIds = await attachmentService.getAttachmentIdsForCases({
|
||||
caseIds: ids,
|
||||
unsecuredSavedObjectsClient,
|
||||
cases: cases.saved_objects.map((caseInfo) => ({
|
||||
id: caseInfo.id,
|
||||
owner: caseInfo.attributes.owner,
|
||||
connectorId: caseInfo.attributes.connector.id,
|
||||
})),
|
||||
user,
|
||||
});
|
||||
|
||||
const userActionIds = await userActionService.getUserActionIdsForCases(ids);
|
||||
|
||||
const bulkDeleteEntities: SavedObjectsBulkDeleteObject[] = [
|
||||
...ids.map((id) => ({ id, type: CASE_SAVED_OBJECT })),
|
||||
...attachmentIds.map((id) => ({ id, type: CASE_COMMENT_SAVED_OBJECT })),
|
||||
...userActionIds.map((id) => ({ id, type: CASE_USER_ACTION_SAVED_OBJECT })),
|
||||
];
|
||||
|
||||
await caseService.bulkDeleteCaseEntities({
|
||||
entities: bulkDeleteEntities,
|
||||
options: { refresh: 'wait_for' },
|
||||
});
|
||||
|
||||
await userActionService.bulkAuditLogCaseDeletion(
|
||||
cases.saved_objects.map((caseInfo) => caseInfo.id)
|
||||
);
|
||||
} catch (error) {
|
||||
throw createCaseError({
|
||||
message: `Failed to delete cases ids: ${JSON.stringify(ids)}: ${error}`,
|
||||
|
|
|
@ -259,7 +259,6 @@ export const push = async (
|
|||
if (shouldMarkAsClosed) {
|
||||
await userActionService.createUserAction({
|
||||
type: ActionTypes.status,
|
||||
unsecuredSavedObjectsClient,
|
||||
payload: { status: CaseStatuses.closed },
|
||||
user,
|
||||
caseId,
|
||||
|
@ -274,7 +273,6 @@ export const push = async (
|
|||
|
||||
await userActionService.createUserAction({
|
||||
type: ActionTypes.pushed,
|
||||
unsecuredSavedObjectsClient,
|
||||
payload: { externalService },
|
||||
user,
|
||||
caseId,
|
||||
|
|
|
@ -305,7 +305,6 @@ export const update = async (
|
|||
clientArgs: CasesClientArgs
|
||||
): Promise<CasesResponse> => {
|
||||
const {
|
||||
unsecuredSavedObjectsClient,
|
||||
services: {
|
||||
caseService,
|
||||
userActionService,
|
||||
|
@ -446,7 +445,6 @@ export const update = async (
|
|||
}, [] as CaseResponse[]);
|
||||
|
||||
await userActionService.bulkCreateUpdateCase({
|
||||
unsecuredSavedObjectsClient,
|
||||
originalCases: myCases.saved_objects,
|
||||
updatedCases: updatedCases.saved_objects,
|
||||
user,
|
||||
|
|
|
@ -14,7 +14,11 @@ import type {
|
|||
IBasePath,
|
||||
} from '@kbn/core/server';
|
||||
import { SECURITY_EXTENSION_ID } from '@kbn/core-saved-objects-server';
|
||||
import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server';
|
||||
import type {
|
||||
AuditLogger,
|
||||
SecurityPluginSetup,
|
||||
SecurityPluginStart,
|
||||
} from '@kbn/security-plugin/server';
|
||||
import type { PluginStartContract as FeaturesPluginStart } from '@kbn/features-plugin/server';
|
||||
import type { PluginStartContract as ActionsPluginStart } from '@kbn/actions-plugin/server';
|
||||
import type { LensServerPluginSetup } from '@kbn/lens-plugin/server';
|
||||
|
@ -119,6 +123,7 @@ export class CasesClientFactory {
|
|||
unsecuredSavedObjectsClient,
|
||||
esClient: scopedClusterClient,
|
||||
request,
|
||||
auditLogger,
|
||||
});
|
||||
|
||||
const userInfo = await this.getUserInfo(request);
|
||||
|
@ -149,10 +154,12 @@ export class CasesClientFactory {
|
|||
unsecuredSavedObjectsClient,
|
||||
esClient,
|
||||
request,
|
||||
auditLogger,
|
||||
}: {
|
||||
unsecuredSavedObjectsClient: SavedObjectsClientContract;
|
||||
esClient: ElasticsearchClient;
|
||||
request: KibanaRequest;
|
||||
auditLogger: AuditLogger;
|
||||
}): CasesServices {
|
||||
this.validateInitialization();
|
||||
|
||||
|
@ -190,10 +197,12 @@ export class CasesClientFactory {
|
|||
caseService,
|
||||
caseConfigureService: new CaseConfigureService(this.logger),
|
||||
connectorMappingsService: new ConnectorMappingsService(this.logger),
|
||||
userActionService: new CaseUserActionService(
|
||||
this.logger,
|
||||
this.options.persistableStateAttachmentTypeRegistry
|
||||
),
|
||||
userActionService: new CaseUserActionService({
|
||||
log: this.logger,
|
||||
persistableStateAttachmentTypeRegistry: this.options.persistableStateAttachmentTypeRegistry,
|
||||
unsecuredSavedObjectsClient,
|
||||
auditLogger,
|
||||
}),
|
||||
attachmentService,
|
||||
licensingService,
|
||||
notificationService,
|
||||
|
|
|
@ -18,7 +18,6 @@ export class Connectors extends SingleCaseBaseHandler {
|
|||
|
||||
public async compute(): Promise<SingleCaseMetricsResponse> {
|
||||
const {
|
||||
unsecuredSavedObjectsClient,
|
||||
authorization,
|
||||
services: { userActionService },
|
||||
logger,
|
||||
|
@ -29,7 +28,6 @@ export class Connectors extends SingleCaseBaseHandler {
|
|||
);
|
||||
|
||||
const uniqueConnectors = await userActionService.getUniqueConnectors({
|
||||
unsecuredSavedObjectsClient,
|
||||
caseId: this.caseId,
|
||||
filter: authorizationFilter,
|
||||
});
|
||||
|
|
|
@ -26,7 +26,6 @@ export class Lifespan extends SingleCaseBaseHandler {
|
|||
|
||||
public async compute(): Promise<SingleCaseMetricsResponse> {
|
||||
const {
|
||||
unsecuredSavedObjectsClient,
|
||||
authorization,
|
||||
services: { userActionService },
|
||||
logger,
|
||||
|
@ -49,7 +48,6 @@ export class Lifespan extends SingleCaseBaseHandler {
|
|||
);
|
||||
|
||||
const statusUserActions = await userActionService.findStatusChanges({
|
||||
unsecuredSavedObjectsClient,
|
||||
caseId: this.caseId,
|
||||
filter: authorizationFilter,
|
||||
});
|
||||
|
|
|
@ -18,17 +18,13 @@ export const get = async (
|
|||
clientArgs: CasesClientArgs
|
||||
): Promise<CaseUserActionsResponse> => {
|
||||
const {
|
||||
unsecuredSavedObjectsClient,
|
||||
services: { userActionService },
|
||||
logger,
|
||||
authorization,
|
||||
} = clientArgs;
|
||||
|
||||
try {
|
||||
const userActions = await userActionService.getAll({
|
||||
unsecuredSavedObjectsClient,
|
||||
caseId,
|
||||
});
|
||||
const userActions = await userActionService.getAll(caseId);
|
||||
|
||||
await authorization.ensureAuthorized({
|
||||
entities: userActions.saved_objects.map((userAction) => ({
|
||||
|
|
|
@ -185,7 +185,6 @@ export class CaseCommentModel {
|
|||
await this.params.services.userActionService.createUserAction({
|
||||
type: ActionTypes.comment,
|
||||
action: Actions.update,
|
||||
unsecuredSavedObjectsClient: this.params.unsecuredSavedObjectsClient,
|
||||
caseId: this.caseInfo.id,
|
||||
attachmentId: comment.id,
|
||||
payload: { attachment: queryRestAttributes },
|
||||
|
@ -339,7 +338,6 @@ export class CaseCommentModel {
|
|||
await this.params.services.userActionService.createUserAction({
|
||||
type: ActionTypes.comment,
|
||||
action: Actions.create,
|
||||
unsecuredSavedObjectsClient: this.params.unsecuredSavedObjectsClient,
|
||||
caseId: this.caseInfo.id,
|
||||
attachmentId: comment.id,
|
||||
payload: {
|
||||
|
@ -352,7 +350,6 @@ export class CaseCommentModel {
|
|||
|
||||
private async bulkCreateCommentUserAction(attachments: Array<{ id: string } & CommentRequest>) {
|
||||
await this.params.services.userActionService.bulkCreateAttachmentCreation({
|
||||
unsecuredSavedObjectsClient: this.params.unsecuredSavedObjectsClient,
|
||||
caseId: this.caseInfo.id,
|
||||
attachments: attachments.map(({ id, ...attachment }) => ({
|
||||
id,
|
||||
|
|
|
@ -100,6 +100,43 @@ export class AttachmentService {
|
|||
private readonly persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry
|
||||
) {}
|
||||
|
||||
public async getAttachmentIdsForCases({
|
||||
caseIds,
|
||||
unsecuredSavedObjectsClient,
|
||||
}: {
|
||||
caseIds: string[];
|
||||
unsecuredSavedObjectsClient: SavedObjectsClientContract;
|
||||
}) {
|
||||
try {
|
||||
this.log.debug(`Attempting to retrieve attachments associated with cases: [${caseIds}]`);
|
||||
|
||||
const finder = unsecuredSavedObjectsClient.createPointInTimeFinder({
|
||||
type: CASE_COMMENT_SAVED_OBJECT,
|
||||
hasReference: caseIds.map((id) => ({ id, type: CASE_SAVED_OBJECT })),
|
||||
sortField: 'created_at',
|
||||
sortOrder: 'asc',
|
||||
/**
|
||||
* We only care about the ids so to reduce the data returned we should limit the fields in the response. Core
|
||||
* doesn't support retrieving no fields (id would always be returned anyway) so to limit it we'll only request
|
||||
* the owner even though we don't need it.
|
||||
*/
|
||||
fields: ['owner'],
|
||||
perPage: MAX_DOCS_PER_PAGE,
|
||||
});
|
||||
|
||||
const ids: string[] = [];
|
||||
|
||||
for await (const attachmentSavedObject of finder.find()) {
|
||||
ids.push(...attachmentSavedObject.saved_objects.map((attachment) => attachment.id));
|
||||
}
|
||||
|
||||
return ids;
|
||||
} catch (error) {
|
||||
this.log.error(`Error retrieving attachments associated with cases: [${caseIds}]: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async countAlertsAttachedToCase(
|
||||
params: AlertsAttachedToCaseArgs
|
||||
): Promise<number | undefined> {
|
||||
|
|
|
@ -15,6 +15,8 @@ import type {
|
|||
SavedObjectsUpdateResponse,
|
||||
SavedObjectsResolveResponse,
|
||||
SavedObjectsFindOptions,
|
||||
SavedObjectsBulkDeleteObject,
|
||||
SavedObjectsBulkDeleteOptions,
|
||||
} from '@kbn/core/server';
|
||||
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
|
@ -308,6 +310,21 @@ export class CasesService {
|
|||
}
|
||||
}
|
||||
|
||||
public async bulkDeleteCaseEntities({
|
||||
entities,
|
||||
options,
|
||||
}: {
|
||||
entities: SavedObjectsBulkDeleteObject[];
|
||||
options?: SavedObjectsBulkDeleteOptions;
|
||||
}) {
|
||||
try {
|
||||
this.log.debug(`Attempting to bulk delete case entities ${JSON.stringify(entities)}`);
|
||||
return await this.unsecuredSavedObjectsClient.bulkDelete(entities, options);
|
||||
} catch (error) {
|
||||
this.log.error(`Error bulk deleting case entities ${JSON.stringify(entities)}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getCase({ id: caseId }: GetCaseArgs): Promise<CaseSavedObject> {
|
||||
try {
|
||||
this.log.debug(`Attempting to GET case ${caseId}`);
|
||||
|
|
|
@ -75,16 +75,15 @@ export const connectorMappingsServiceMock = (): ConnectorMappingsServiceMock =>
|
|||
|
||||
export const createUserActionServiceMock = (): CaseUserActionServiceMock => {
|
||||
const service: PublicMethodsOf<CaseUserActionService> = {
|
||||
bulkCreateCaseDeletion: jest.fn(),
|
||||
bulkAuditLogCaseDeletion: jest.fn(),
|
||||
bulkCreateUpdateCase: jest.fn(),
|
||||
bulkCreateAttachmentDeletion: jest.fn(),
|
||||
bulkCreateAttachmentCreation: jest.fn(),
|
||||
createUserAction: jest.fn(),
|
||||
create: jest.fn(),
|
||||
getAll: jest.fn(),
|
||||
bulkCreate: jest.fn(),
|
||||
findStatusChanges: jest.fn(),
|
||||
getUniqueConnectors: jest.fn(),
|
||||
getUserActionIdsForCases: jest.fn(),
|
||||
};
|
||||
|
||||
// the cast here is required because jest.Mocked tries to include private members and would throw an error
|
||||
|
@ -117,6 +116,7 @@ export const createAttachmentServiceMock = (): AttachmentServiceMock => {
|
|||
getCaseCommentStats: jest.fn(),
|
||||
valueCountAlertsAttachedToCase: jest.fn(),
|
||||
executeCaseAggregations: jest.fn(),
|
||||
getAttachmentIdsForCases: jest.fn(),
|
||||
};
|
||||
|
||||
// the cast here is required because jest.Mocked tries to include private members and would throw an error
|
||||
|
|
116
x-pack/plugins/cases/server/services/user_actions/__snapshots__/audit_logger.test.ts.snap
generated
Normal file
116
x-pack/plugins/cases/server/services/user_actions/__snapshots__/audit_logger.test.ts.snap
generated
Normal file
|
@ -0,0 +1,116 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`UserActionAuditLogger logs add user action as event.type change 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "action",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"change",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "123",
|
||||
"type": "type",
|
||||
},
|
||||
},
|
||||
"message": "id: idParam",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`UserActionAuditLogger logs create user action as event.type creation 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "action",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"creation",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "123",
|
||||
"type": "type",
|
||||
},
|
||||
},
|
||||
"message": "id: idParam",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`UserActionAuditLogger logs delete user action as event.type deletion 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "action",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"deletion",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "123",
|
||||
"type": "type",
|
||||
},
|
||||
},
|
||||
"message": "id: idParam",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`UserActionAuditLogger logs push_to_service user action as event.type creation 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "action",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"creation",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "123",
|
||||
"type": "type",
|
||||
},
|
||||
},
|
||||
"message": "id: idParam",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`UserActionAuditLogger logs update user action as event.type change 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "action",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"change",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "123",
|
||||
"type": "type",
|
||||
},
|
||||
},
|
||||
"message": "id: idParam",
|
||||
},
|
||||
]
|
||||
`;
|
70
x-pack/plugins/cases/server/services/user_actions/__snapshots__/index.test.ts.snap
generated
Normal file
70
x-pack/plugins/cases/server/services/user_actions/__snapshots__/index.test.ts.snap
generated
Normal file
|
@ -0,0 +1,70 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CaseUserActionService methods createUserAction create case comment logs a comment user action of action: create 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_action_create_comment",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"creation",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "test-id",
|
||||
"type": "cases-comments",
|
||||
},
|
||||
},
|
||||
"message": "User created comment id: test-id for case id: 123 - user action id: created_user_action_id",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`CaseUserActionService methods createUserAction create case comment logs a comment user action of action: delete 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_action_delete_comment",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"deletion",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "test-id",
|
||||
"type": "cases-comments",
|
||||
},
|
||||
},
|
||||
"message": "User deleted comment id: test-id for case id: 123 - user action id: created_user_action_id",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`CaseUserActionService methods createUserAction create case comment logs a comment user action of action: update 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_action_update_comment",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"change",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "test-id",
|
||||
"type": "cases-comments",
|
||||
},
|
||||
},
|
||||
"message": "User changed comment id: test-id for case id: 123 - user action id: created_user_action_id",
|
||||
},
|
||||
]
|
||||
`;
|
|
@ -19,9 +19,10 @@ import { ActionTypes, NONE_CONNECTOR_ID } from '../../../common/api';
|
|||
import type {
|
||||
BuilderDeps,
|
||||
BuilderParameters,
|
||||
BuilderReturnValue,
|
||||
CommonBuilderArguments,
|
||||
SavedObjectParameters,
|
||||
UserActionParameters,
|
||||
UserActionEvent,
|
||||
} from './types';
|
||||
import type { PersistableStateAttachmentTypeRegistry } from '../../attachment_framework/persistable_state_registry';
|
||||
|
||||
|
@ -98,7 +99,7 @@ export abstract class UserActionBuilder {
|
|||
attachmentId,
|
||||
connectorId,
|
||||
type,
|
||||
}: CommonBuilderArguments): BuilderReturnValue => {
|
||||
}: CommonBuilderArguments): SavedObjectParameters => {
|
||||
return {
|
||||
attributes: {
|
||||
...this.getCommonUserActionAttributes({ user, owner }),
|
||||
|
@ -121,5 +122,5 @@ export abstract class UserActionBuilder {
|
|||
|
||||
public abstract build<T extends keyof BuilderParameters>(
|
||||
args: UserActionParameters<T>
|
||||
): BuilderReturnValue;
|
||||
): UserActionEvent;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Actions } from '../../../common/api';
|
||||
import type { AuditLogger } from '@kbn/security-plugin/server';
|
||||
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
||||
import { UserActionAuditLogger } from './audit_logger';
|
||||
import type { EventDetails } from './types';
|
||||
|
||||
describe('UserActionAuditLogger', () => {
|
||||
let mockLogger: jest.Mocked<AuditLogger>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockLogger = auditLoggerMock.create();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[Actions.add, 'change'],
|
||||
[Actions.create, 'creation'],
|
||||
[Actions.delete, 'deletion'],
|
||||
[Actions.push_to_service, 'creation'],
|
||||
[Actions.update, 'change'],
|
||||
])('logs %s user action as event.type %s', (action, type) => {
|
||||
const eventDetails: EventDetails = {
|
||||
getMessage: (id?: string) => `id: ${id}`,
|
||||
action,
|
||||
descriptiveAction: 'action',
|
||||
savedObjectId: '123',
|
||||
savedObjectType: 'type',
|
||||
};
|
||||
|
||||
const logger = new UserActionAuditLogger(mockLogger);
|
||||
logger.log(eventDetails, 'idParam');
|
||||
|
||||
expect(mockLogger.log.mock.calls[0]).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('does not call the internal audit logger when the event details are undefined', () => {
|
||||
const logger = new UserActionAuditLogger(mockLogger);
|
||||
|
||||
logger.log();
|
||||
|
||||
expect(mockLogger.log).not.toBeCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { EcsEventType } from '@kbn/logging';
|
||||
import type { AuditLogger } from '@kbn/security-plugin/server';
|
||||
import type { UserAction as Action } from '../../../common/api';
|
||||
import type { EventDetails } from './types';
|
||||
|
||||
const actionsToEcsType: Record<Action, EcsEventType> = {
|
||||
add: 'change',
|
||||
delete: 'deletion',
|
||||
create: 'creation',
|
||||
push_to_service: 'creation',
|
||||
update: 'change',
|
||||
};
|
||||
|
||||
export class UserActionAuditLogger {
|
||||
constructor(private readonly auditLogger: AuditLogger) {}
|
||||
|
||||
public log(event?: EventDetails, storedUserActionId?: string) {
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.auditLogger.log({
|
||||
message: event.getMessage(storedUserActionId),
|
||||
event: {
|
||||
action: event.descriptiveAction,
|
||||
category: ['database'],
|
||||
type: [actionsToEcsType[event.action]],
|
||||
},
|
||||
kibana: {
|
||||
saved_object: {
|
||||
type: event.savedObjectType,
|
||||
id: event.savedObjectId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -5,18 +5,54 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CASE_SAVED_OBJECT } from '../../../../common/constants';
|
||||
import type { UserAction } from '../../../../common/api';
|
||||
import { ActionTypes, Actions } from '../../../../common/api';
|
||||
import { UserActionBuilder } from '../abstract_builder';
|
||||
import type { UserActionParameters, BuilderReturnValue } from '../types';
|
||||
import type { EventDetails, UserActionParameters, UserActionEvent } from '../types';
|
||||
|
||||
export class AssigneesUserActionBuilder extends UserActionBuilder {
|
||||
build(args: UserActionParameters<'assignees'>): BuilderReturnValue {
|
||||
return this.buildCommonUserAction({
|
||||
build(args: UserActionParameters<'assignees'>): UserActionEvent {
|
||||
const action = args.action ?? Actions.add;
|
||||
|
||||
const soParams = this.buildCommonUserAction({
|
||||
...args,
|
||||
action: args.action ?? Actions.add,
|
||||
action,
|
||||
valueKey: 'assignees',
|
||||
value: args.payload.assignees,
|
||||
type: ActionTypes.assignees,
|
||||
});
|
||||
|
||||
const uids = args.payload.assignees.map((assignee) => assignee.uid);
|
||||
const verbMessage = getVerbMessage(action, uids);
|
||||
|
||||
const getMessage = (id?: string) =>
|
||||
`User ${verbMessage} case id: ${args.caseId} - user action id: ${id}`;
|
||||
|
||||
const event: EventDetails = {
|
||||
getMessage,
|
||||
action,
|
||||
descriptiveAction: `case_user_action_${action}_case_assignees`,
|
||||
savedObjectId: args.caseId,
|
||||
savedObjectType: CASE_SAVED_OBJECT,
|
||||
};
|
||||
|
||||
return {
|
||||
parameters: soParams,
|
||||
eventDetails: event,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const getVerbMessage = (action: UserAction, uids: string[]) => {
|
||||
const uidText = `uids: [${uids}]`;
|
||||
|
||||
switch (action) {
|
||||
case 'add':
|
||||
return `assigned ${uidText} to`;
|
||||
case 'delete':
|
||||
return `unassigned ${uidText} from`;
|
||||
default:
|
||||
return `changed ${uidText} for`;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { UserAction as Action } from '../../../../common/api';
|
||||
|
||||
const actionsToVerbs: Record<Action, string> = {
|
||||
add: 'added',
|
||||
delete: 'deleted',
|
||||
create: 'created',
|
||||
push_to_service: 'pushed',
|
||||
update: 'changed',
|
||||
};
|
||||
|
||||
export const getPastTenseVerb = (action: Action): string => actionsToVerbs[action];
|
|
@ -6,15 +6,17 @@
|
|||
*/
|
||||
|
||||
import { uniqBy } from 'lodash';
|
||||
import { CASE_COMMENT_SAVED_OBJECT } from '../../../../common/constants';
|
||||
import { extractPersistableStateReferencesFromSO } from '../../../attachment_framework/so_references';
|
||||
import type { CommentUserAction } from '../../../../common/api';
|
||||
import { ActionTypes, Actions } from '../../../../common/api';
|
||||
import { UserActionBuilder } from '../abstract_builder';
|
||||
import type { UserActionParameters, BuilderReturnValue } from '../types';
|
||||
import type { EventDetails, UserActionParameters, UserActionEvent } from '../types';
|
||||
import { getAttachmentSOExtractor } from '../../so_references';
|
||||
import { getPastTenseVerb } from './audit_logger_utils';
|
||||
|
||||
export class CommentUserActionBuilder extends UserActionBuilder {
|
||||
build(args: UserActionParameters<'comment'>): BuilderReturnValue {
|
||||
build(args: UserActionParameters<'comment'>): UserActionEvent {
|
||||
const soExtractor = getAttachmentSOExtractor(args.payload.attachment);
|
||||
const { transformedFields, references: refsWithExternalRefId } =
|
||||
soExtractor.extractFieldsToReferences<CommentUserAction['payload']['comment']>({
|
||||
|
@ -26,20 +28,46 @@ export class CommentUserActionBuilder extends UserActionBuilder {
|
|||
persistableStateAttachmentTypeRegistry: this.persistableStateAttachmentTypeRegistry,
|
||||
});
|
||||
|
||||
const action = args.action ?? Actions.update;
|
||||
|
||||
const commentUserAction = this.buildCommonUserAction({
|
||||
...args,
|
||||
action: args.action ?? Actions.update,
|
||||
action,
|
||||
valueKey: 'comment',
|
||||
value: { ...transformedFields, ...extractedAttributes },
|
||||
type: ActionTypes.comment,
|
||||
});
|
||||
|
||||
return {
|
||||
const parameters = {
|
||||
...commentUserAction,
|
||||
references: uniqBy(
|
||||
[...commentUserAction.references, ...refsWithExternalRefId, ...extractedReferences],
|
||||
'id'
|
||||
),
|
||||
};
|
||||
|
||||
const verb = getPastTenseVerb(action);
|
||||
|
||||
const getMessage = (id?: string) =>
|
||||
`User ${verb} comment id: ${commentId(args.attachmentId)} for case id: ${
|
||||
args.caseId
|
||||
} - user action id: ${id}`;
|
||||
|
||||
const eventDetails: EventDetails = {
|
||||
getMessage,
|
||||
action,
|
||||
descriptiveAction: `case_user_action_${action}_comment`,
|
||||
savedObjectId: args.attachmentId ?? args.caseId,
|
||||
savedObjectType: CASE_COMMENT_SAVED_OBJECT,
|
||||
};
|
||||
|
||||
return {
|
||||
parameters,
|
||||
eventDetails,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const commentId = (id?: string) => {
|
||||
return id ? id : 'unknown';
|
||||
};
|
||||
|
|
|
@ -5,19 +5,38 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CASE_SAVED_OBJECT } from '../../../../common/constants';
|
||||
import { Actions, ActionTypes } from '../../../../common/api';
|
||||
import { UserActionBuilder } from '../abstract_builder';
|
||||
import type { UserActionParameters, BuilderReturnValue } from '../types';
|
||||
import type { EventDetails, UserActionParameters, UserActionEvent } from '../types';
|
||||
|
||||
export class ConnectorUserActionBuilder extends UserActionBuilder {
|
||||
build(args: UserActionParameters<'connector'>): BuilderReturnValue {
|
||||
return this.buildCommonUserAction({
|
||||
build(args: UserActionParameters<'connector'>): UserActionEvent {
|
||||
const action = Actions.update;
|
||||
|
||||
const parameters = this.buildCommonUserAction({
|
||||
...args,
|
||||
action: Actions.update,
|
||||
action,
|
||||
valueKey: 'connector',
|
||||
value: this.extractConnectorId(args.payload.connector),
|
||||
type: ActionTypes.connector,
|
||||
connectorId: args.payload.connector.id,
|
||||
});
|
||||
|
||||
const getMessage = (id?: string) =>
|
||||
`User changed the case connector to id: ${args.payload.connector.id} for case id: ${args.caseId} - user action id: ${id}`;
|
||||
|
||||
const eventDetails: EventDetails = {
|
||||
getMessage,
|
||||
action,
|
||||
descriptiveAction: 'case_user_action_update_case_connector',
|
||||
savedObjectId: args.caseId,
|
||||
savedObjectType: CASE_SAVED_OBJECT,
|
||||
};
|
||||
|
||||
return {
|
||||
parameters,
|
||||
eventDetails,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,18 +5,21 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CASE_SAVED_OBJECT } from '../../../../common/constants';
|
||||
import { Actions, ActionTypes, CaseStatuses } from '../../../../common/api';
|
||||
import { UserActionBuilder } from '../abstract_builder';
|
||||
import type { UserActionParameters, BuilderReturnValue } from '../types';
|
||||
import type { EventDetails, UserActionParameters, UserActionEvent } from '../types';
|
||||
|
||||
export class CreateCaseUserActionBuilder extends UserActionBuilder {
|
||||
build(args: UserActionParameters<'create_case'>): BuilderReturnValue {
|
||||
build(args: UserActionParameters<'create_case'>): UserActionEvent {
|
||||
const { payload, caseId, owner, user } = args;
|
||||
const action = Actions.create;
|
||||
|
||||
const connectorWithoutId = this.extractConnectorId(payload.connector);
|
||||
return {
|
||||
const parameters = {
|
||||
attributes: {
|
||||
...this.getCommonUserActionAttributes({ user, owner }),
|
||||
action: Actions.create,
|
||||
action,
|
||||
payload: { ...payload, connector: connectorWithoutId, status: CaseStatuses.open },
|
||||
type: ActionTypes.create_case,
|
||||
},
|
||||
|
@ -25,5 +28,20 @@ export class CreateCaseUserActionBuilder extends UserActionBuilder {
|
|||
...this.createConnectorReference(payload.connector.id),
|
||||
],
|
||||
};
|
||||
|
||||
const getMessage = (id?: string) => `User created case id: ${caseId} - user action id: ${id}`;
|
||||
|
||||
const eventDetails: EventDetails = {
|
||||
getMessage,
|
||||
action,
|
||||
descriptiveAction: 'case_user_action_create_case',
|
||||
savedObjectId: caseId,
|
||||
savedObjectType: CASE_SAVED_OBJECT,
|
||||
};
|
||||
|
||||
return {
|
||||
parameters,
|
||||
eventDetails,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,17 +5,26 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CASE_SAVED_OBJECT } from '../../../../common/constants';
|
||||
import type { UserAction } from '../../../../common/api';
|
||||
import { Actions, ActionTypes } from '../../../../common/api';
|
||||
import { UserActionBuilder } from '../abstract_builder';
|
||||
import type { UserActionParameters, BuilderReturnValue } from '../types';
|
||||
import type {
|
||||
SavedObjectParameters,
|
||||
EventDetails,
|
||||
UserActionParameters,
|
||||
UserActionEvent,
|
||||
} from '../types';
|
||||
|
||||
export class DeleteCaseUserActionBuilder extends UserActionBuilder {
|
||||
build(args: UserActionParameters<'delete_case'>): BuilderReturnValue {
|
||||
build(args: UserActionParameters<'delete_case'>): UserActionEvent {
|
||||
const { caseId, owner, user, connectorId } = args;
|
||||
return {
|
||||
const action = Actions.delete;
|
||||
|
||||
const parameters: SavedObjectParameters = {
|
||||
attributes: {
|
||||
...this.getCommonUserActionAttributes({ user, owner }),
|
||||
action: Actions.delete,
|
||||
action,
|
||||
payload: {},
|
||||
type: ActionTypes.delete_case,
|
||||
},
|
||||
|
@ -24,5 +33,24 @@ export class DeleteCaseUserActionBuilder extends UserActionBuilder {
|
|||
...this.createConnectorReference(connectorId ?? null),
|
||||
],
|
||||
};
|
||||
|
||||
return {
|
||||
parameters,
|
||||
eventDetails: createDeleteEvent({ caseId, action }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const createDeleteEvent = ({
|
||||
caseId,
|
||||
action,
|
||||
}: {
|
||||
caseId: string;
|
||||
action: UserAction;
|
||||
}): EventDetails => ({
|
||||
getMessage: () => `User deleted case id: ${caseId}`,
|
||||
action,
|
||||
descriptiveAction: 'case_user_action_delete_case',
|
||||
savedObjectId: caseId,
|
||||
savedObjectType: CASE_SAVED_OBJECT,
|
||||
});
|
||||
|
|
|
@ -5,18 +5,37 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CASE_SAVED_OBJECT } from '../../../../common/constants';
|
||||
import { Actions, ActionTypes } from '../../../../common/api';
|
||||
import { UserActionBuilder } from '../abstract_builder';
|
||||
import type { UserActionParameters, BuilderReturnValue } from '../types';
|
||||
import type { EventDetails, UserActionParameters, UserActionEvent } from '../types';
|
||||
|
||||
export class DescriptionUserActionBuilder extends UserActionBuilder {
|
||||
build(args: UserActionParameters<'description'>): BuilderReturnValue {
|
||||
return this.buildCommonUserAction({
|
||||
build(args: UserActionParameters<'description'>): UserActionEvent {
|
||||
const action = Actions.update;
|
||||
|
||||
const parameters = this.buildCommonUserAction({
|
||||
...args,
|
||||
action: Actions.update,
|
||||
action,
|
||||
valueKey: 'description',
|
||||
type: ActionTypes.description,
|
||||
value: args.payload.description,
|
||||
});
|
||||
|
||||
const getMessage = (id?: string) =>
|
||||
`User updated the description for case id: ${args.caseId} - user action id: ${id}`;
|
||||
|
||||
const eventDetails: EventDetails = {
|
||||
getMessage,
|
||||
action,
|
||||
descriptiveAction: 'case_user_action_update_case_description',
|
||||
savedObjectId: args.caseId,
|
||||
savedObjectType: CASE_SAVED_OBJECT,
|
||||
};
|
||||
|
||||
return {
|
||||
parameters,
|
||||
eventDetails,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,19 +5,38 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CASE_SAVED_OBJECT } from '../../../../common/constants';
|
||||
import { Actions, ActionTypes } from '../../../../common/api';
|
||||
import { UserActionBuilder } from '../abstract_builder';
|
||||
import type { UserActionParameters, BuilderReturnValue } from '../types';
|
||||
import type { EventDetails, UserActionParameters, UserActionEvent } from '../types';
|
||||
|
||||
export class PushedUserActionBuilder extends UserActionBuilder {
|
||||
build(args: UserActionParameters<'pushed'>): BuilderReturnValue {
|
||||
return this.buildCommonUserAction({
|
||||
build(args: UserActionParameters<'pushed'>): UserActionEvent {
|
||||
const action = Actions.push_to_service;
|
||||
|
||||
const parameters = this.buildCommonUserAction({
|
||||
...args,
|
||||
action: Actions.push_to_service,
|
||||
action,
|
||||
valueKey: 'externalService',
|
||||
value: this.extractConnectorIdFromExternalService(args.payload.externalService),
|
||||
type: ActionTypes.pushed,
|
||||
connectorId: args.payload.externalService.connector_id,
|
||||
});
|
||||
|
||||
const getMessage = (id?: string) =>
|
||||
`User pushed case id: ${args.caseId} to an external service with connector id: ${args.payload.externalService.connector_id} - user action id: ${id}`;
|
||||
|
||||
const eventDetails: EventDetails = {
|
||||
getMessage,
|
||||
action,
|
||||
descriptiveAction: 'case_user_action_pushed_case',
|
||||
savedObjectId: args.caseId,
|
||||
savedObjectType: CASE_SAVED_OBJECT,
|
||||
};
|
||||
|
||||
return {
|
||||
parameters,
|
||||
eventDetails,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,18 +5,37 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CASE_SAVED_OBJECT } from '../../../../common/constants';
|
||||
import { Actions, ActionTypes } from '../../../../common/api';
|
||||
import { UserActionBuilder } from '../abstract_builder';
|
||||
import type { UserActionParameters, BuilderReturnValue } from '../types';
|
||||
import type { EventDetails, UserActionParameters, UserActionEvent } from '../types';
|
||||
|
||||
export class SettingsUserActionBuilder extends UserActionBuilder {
|
||||
build(args: UserActionParameters<'settings'>): BuilderReturnValue {
|
||||
return this.buildCommonUserAction({
|
||||
build(args: UserActionParameters<'settings'>): UserActionEvent {
|
||||
const action = Actions.update;
|
||||
|
||||
const parameters = this.buildCommonUserAction({
|
||||
...args,
|
||||
action: Actions.update,
|
||||
action,
|
||||
valueKey: 'settings',
|
||||
value: args.payload.settings,
|
||||
type: ActionTypes.settings,
|
||||
});
|
||||
|
||||
const getMessage = (id?: string) =>
|
||||
`User updated the settings for case id: ${args.caseId} - user action id: ${id}`;
|
||||
|
||||
const eventDetails: EventDetails = {
|
||||
getMessage,
|
||||
action,
|
||||
descriptiveAction: 'case_user_action_update_case_settings',
|
||||
savedObjectId: args.caseId,
|
||||
savedObjectType: CASE_SAVED_OBJECT,
|
||||
};
|
||||
|
||||
return {
|
||||
parameters,
|
||||
eventDetails,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,18 +5,37 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CASE_SAVED_OBJECT } from '../../../../common/constants';
|
||||
import { Actions, ActionTypes } from '../../../../common/api';
|
||||
import { UserActionBuilder } from '../abstract_builder';
|
||||
import type { UserActionParameters, BuilderReturnValue } from '../types';
|
||||
import type { EventDetails, UserActionParameters, UserActionEvent } from '../types';
|
||||
|
||||
export class SeverityUserActionBuilder extends UserActionBuilder {
|
||||
build(args: UserActionParameters<'severity'>): BuilderReturnValue {
|
||||
return this.buildCommonUserAction({
|
||||
build(args: UserActionParameters<'severity'>): UserActionEvent {
|
||||
const action = Actions.update;
|
||||
|
||||
const parameters = this.buildCommonUserAction({
|
||||
...args,
|
||||
action: Actions.update,
|
||||
action,
|
||||
valueKey: 'severity',
|
||||
value: args.payload.severity,
|
||||
type: ActionTypes.severity,
|
||||
});
|
||||
|
||||
const getMessage = (id?: string) =>
|
||||
`User updated the severity for case id: ${args.caseId} - user action id: ${id}`;
|
||||
|
||||
const eventDetails: EventDetails = {
|
||||
getMessage,
|
||||
action,
|
||||
descriptiveAction: 'case_user_action_update_case_severity',
|
||||
savedObjectId: args.caseId,
|
||||
savedObjectType: CASE_SAVED_OBJECT,
|
||||
};
|
||||
|
||||
return {
|
||||
parameters,
|
||||
eventDetails,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,18 +5,37 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CASE_SAVED_OBJECT } from '../../../../common/constants';
|
||||
import { Actions, ActionTypes } from '../../../../common/api';
|
||||
import { UserActionBuilder } from '../abstract_builder';
|
||||
import type { UserActionParameters, BuilderReturnValue } from '../types';
|
||||
import type { EventDetails, UserActionParameters, UserActionEvent } from '../types';
|
||||
|
||||
export class StatusUserActionBuilder extends UserActionBuilder {
|
||||
build(args: UserActionParameters<'status'>): BuilderReturnValue {
|
||||
return this.buildCommonUserAction({
|
||||
build(args: UserActionParameters<'status'>): UserActionEvent {
|
||||
const action = Actions.update;
|
||||
|
||||
const parameters = this.buildCommonUserAction({
|
||||
...args,
|
||||
action: Actions.update,
|
||||
action,
|
||||
valueKey: 'status',
|
||||
value: args.payload.status,
|
||||
type: ActionTypes.status,
|
||||
});
|
||||
|
||||
const getMessage = (id?: string) =>
|
||||
`User updated the status for case id: ${args.caseId} - user action id: ${id}`;
|
||||
|
||||
const eventDetails: EventDetails = {
|
||||
getMessage,
|
||||
action,
|
||||
descriptiveAction: 'case_user_action_update_case_status',
|
||||
savedObjectId: args.caseId,
|
||||
savedObjectType: CASE_SAVED_OBJECT,
|
||||
};
|
||||
|
||||
return {
|
||||
parameters,
|
||||
eventDetails,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,18 +5,53 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CASE_SAVED_OBJECT } from '../../../../common/constants';
|
||||
import type { UserAction } from '../../../../common/api';
|
||||
import { ActionTypes, Actions } from '../../../../common/api';
|
||||
import { UserActionBuilder } from '../abstract_builder';
|
||||
import type { UserActionParameters, BuilderReturnValue } from '../types';
|
||||
import type { EventDetails, UserActionParameters, UserActionEvent } from '../types';
|
||||
import { getPastTenseVerb } from './audit_logger_utils';
|
||||
|
||||
export class TagsUserActionBuilder extends UserActionBuilder {
|
||||
build(args: UserActionParameters<'tags'>): BuilderReturnValue {
|
||||
return this.buildCommonUserAction({
|
||||
build(args: UserActionParameters<'tags'>): UserActionEvent {
|
||||
const action = args.action ?? Actions.add;
|
||||
|
||||
const parameters = this.buildCommonUserAction({
|
||||
...args,
|
||||
action: args.action ?? Actions.add,
|
||||
valueKey: 'tags',
|
||||
value: args.payload.tags,
|
||||
type: ActionTypes.tags,
|
||||
});
|
||||
|
||||
const verb = getPastTenseVerb(action);
|
||||
const preposition = getPreposition(action);
|
||||
|
||||
const getMessage = (id?: string) =>
|
||||
`User ${verb} tags ${preposition} case id: ${args.caseId} - user action id: ${id}`;
|
||||
|
||||
const eventDetails: EventDetails = {
|
||||
getMessage,
|
||||
action,
|
||||
descriptiveAction: `case_user_action_${action}_case_tags`,
|
||||
savedObjectId: args.caseId,
|
||||
savedObjectType: CASE_SAVED_OBJECT,
|
||||
};
|
||||
|
||||
return {
|
||||
parameters,
|
||||
eventDetails,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const getPreposition = (action: UserAction): string => {
|
||||
switch (action) {
|
||||
case Actions.add:
|
||||
return 'to';
|
||||
case Actions.delete:
|
||||
return 'in';
|
||||
default:
|
||||
return 'for';
|
||||
}
|
||||
};
|
||||
|
|
|
@ -5,18 +5,37 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CASE_SAVED_OBJECT } from '../../../../common/constants';
|
||||
import { Actions, ActionTypes } from '../../../../common/api';
|
||||
import { UserActionBuilder } from '../abstract_builder';
|
||||
import type { BuilderReturnValue, UserActionParameters } from '../types';
|
||||
import type { EventDetails, UserActionParameters, UserActionEvent } from '../types';
|
||||
|
||||
export class TitleUserActionBuilder extends UserActionBuilder {
|
||||
build(args: UserActionParameters<'title'>): BuilderReturnValue {
|
||||
return this.buildCommonUserAction({
|
||||
build(args: UserActionParameters<'title'>): UserActionEvent {
|
||||
const action = Actions.update;
|
||||
|
||||
const parameters = this.buildCommonUserAction({
|
||||
...args,
|
||||
action: Actions.update,
|
||||
action,
|
||||
valueKey: 'title',
|
||||
value: args.payload.title,
|
||||
type: ActionTypes.title,
|
||||
});
|
||||
|
||||
const getMessage = (id?: string) =>
|
||||
`User updated the title for case id: ${args.caseId} - user action id: ${id}`;
|
||||
|
||||
const eventDetails: EventDetails = {
|
||||
getMessage,
|
||||
action,
|
||||
descriptiveAction: 'case_user_action_update_case_title',
|
||||
savedObjectId: args.caseId,
|
||||
savedObjectType: CASE_SAVED_OBJECT,
|
||||
};
|
||||
|
||||
return {
|
||||
parameters,
|
||||
eventDetails,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,11 +11,13 @@ import { savedObjectsClientMock } from '@kbn/core/server/mocks';
|
|||
import type {
|
||||
SavedObject,
|
||||
SavedObjectReference,
|
||||
SavedObjectsBulkCreateObject,
|
||||
SavedObjectsFindResponse,
|
||||
SavedObjectsFindResult,
|
||||
SavedObjectsUpdateResponse,
|
||||
} from '@kbn/core/server';
|
||||
import { ACTION_SAVED_OBJECT_TYPE } from '@kbn/actions-plugin/server';
|
||||
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
||||
import type {
|
||||
CaseAttributes,
|
||||
CaseUserActionAttributes,
|
||||
|
@ -635,17 +637,38 @@ describe('CaseUserActionService', () => {
|
|||
describe('methods', () => {
|
||||
let service: CaseUserActionService;
|
||||
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValue({
|
||||
id: 'created_user_action_id',
|
||||
} as SavedObject);
|
||||
|
||||
unsecuredSavedObjectsClient.bulkCreate.mockImplementation(
|
||||
async (objects: SavedObjectsBulkCreateObject[]) => {
|
||||
const savedObjects: SavedObject[] = [];
|
||||
for (let i = 0; i < objects.length; i++) {
|
||||
savedObjects.push({ id: i } as unknown as SavedObject);
|
||||
}
|
||||
|
||||
return {
|
||||
saved_objects: savedObjects,
|
||||
};
|
||||
}
|
||||
);
|
||||
const mockLogger = loggerMock.create();
|
||||
const commonArgs = {
|
||||
unsecuredSavedObjectsClient,
|
||||
caseId: '123',
|
||||
user: { full_name: 'Elastic User', username: 'elastic', email: 'elastic@elastic.co' },
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
};
|
||||
const mockAuditLogger = auditLoggerMock.create();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
service = new CaseUserActionService(mockLogger, persistableStateAttachmentTypeRegistry);
|
||||
service = new CaseUserActionService({
|
||||
unsecuredSavedObjectsClient,
|
||||
log: mockLogger,
|
||||
persistableStateAttachmentTypeRegistry,
|
||||
auditLogger: mockAuditLogger,
|
||||
});
|
||||
});
|
||||
|
||||
describe('createUserAction', () => {
|
||||
|
@ -702,6 +725,38 @@ describe('CaseUserActionService', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('logs a create case user action', async () => {
|
||||
await service.createUserAction({
|
||||
...commonArgs,
|
||||
payload: casePayload,
|
||||
type: ActionTypes.create_case,
|
||||
});
|
||||
|
||||
expect(mockAuditLogger.log).toBeCalledTimes(1);
|
||||
expect(mockAuditLogger.log.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_action_create_case",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"creation",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "123",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "User created case id: 123 - user action id: created_user_action_id",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
describe('status', () => {
|
||||
it('creates an update status user action', async () => {
|
||||
await service.createUserAction({
|
||||
|
@ -727,6 +782,38 @@ describe('CaseUserActionService', () => {
|
|||
{ references: [{ id: '123', name: 'associated-cases', type: 'cases' }] }
|
||||
);
|
||||
});
|
||||
|
||||
it('logs an update status user action', async () => {
|
||||
await service.createUserAction({
|
||||
...commonArgs,
|
||||
payload: { status: CaseStatuses.closed },
|
||||
type: ActionTypes.status,
|
||||
});
|
||||
|
||||
expect(mockAuditLogger.log).toBeCalledTimes(1);
|
||||
expect(mockAuditLogger.log.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_action_update_case_status",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"change",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "123",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "User updated the status for case id: 123 - user action id: created_user_action_id",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('severity', () => {
|
||||
|
@ -754,6 +841,38 @@ describe('CaseUserActionService', () => {
|
|||
{ references: [{ id: '123', name: 'associated-cases', type: 'cases' }] }
|
||||
);
|
||||
});
|
||||
|
||||
it('logs an update severity user action', async () => {
|
||||
await service.createUserAction({
|
||||
...commonArgs,
|
||||
payload: { severity: CaseSeverity.MEDIUM },
|
||||
type: ActionTypes.severity,
|
||||
});
|
||||
|
||||
expect(mockAuditLogger.log).toBeCalledTimes(1);
|
||||
expect(mockAuditLogger.log.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_action_update_case_severity",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"change",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "123",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "User updated the severity for case id: 123 - user action id: created_user_action_id",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('push', () => {
|
||||
|
@ -800,6 +919,38 @@ describe('CaseUserActionService', () => {
|
|||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('logs a push user action', async () => {
|
||||
await service.createUserAction({
|
||||
...commonArgs,
|
||||
payload: { externalService },
|
||||
type: ActionTypes.pushed,
|
||||
});
|
||||
|
||||
expect(mockAuditLogger.log).toBeCalledTimes(1);
|
||||
expect(mockAuditLogger.log.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_action_pushed_case",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"creation",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "123",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "User pushed case id: 123 to an external service with connector id: 456 - user action id: created_user_action_id",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('comment', () => {
|
||||
|
@ -843,64 +994,77 @@ describe('CaseUserActionService', () => {
|
|||
);
|
||||
}
|
||||
);
|
||||
|
||||
it.each([[Actions.create], [Actions.delete], [Actions.update]])(
|
||||
'logs a comment user action of action: %s',
|
||||
async (action) => {
|
||||
await service.createUserAction({
|
||||
...commonArgs,
|
||||
type: ActionTypes.comment,
|
||||
action,
|
||||
attachmentId: 'test-id',
|
||||
payload: { attachment: comment },
|
||||
});
|
||||
|
||||
expect(mockAuditLogger.log).toBeCalledTimes(1);
|
||||
expect(mockAuditLogger.log.mock.calls[0]).toMatchSnapshot();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulkCreateCaseDeletion', () => {
|
||||
it('creates a delete case user action', async () => {
|
||||
await service.bulkCreateCaseDeletion({
|
||||
unsecuredSavedObjectsClient,
|
||||
cases: [
|
||||
{ id: '1', owner: SECURITY_SOLUTION_OWNER, connectorId: '3' },
|
||||
{ id: '2', owner: SECURITY_SOLUTION_OWNER, connectorId: '4' },
|
||||
],
|
||||
user: commonArgs.user,
|
||||
});
|
||||
describe('bulkAuditLogCaseDeletion', () => {
|
||||
it('logs a delete case audit log message', async () => {
|
||||
await service.bulkAuditLogCaseDeletion(['1', '2']);
|
||||
|
||||
expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
attributes: {
|
||||
action: 'delete',
|
||||
created_at: '2022-01-09T22:00:00.000Z',
|
||||
created_by: {
|
||||
email: 'elastic@elastic.co',
|
||||
full_name: 'Elastic User',
|
||||
username: 'elastic',
|
||||
expect(unsecuredSavedObjectsClient.bulkCreate).not.toHaveBeenCalled();
|
||||
|
||||
expect(mockAuditLogger.log).toHaveBeenCalledTimes(2);
|
||||
expect(mockAuditLogger.log.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_action_delete_case",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"deletion",
|
||||
],
|
||||
},
|
||||
type: 'delete_case',
|
||||
owner: 'securitySolution',
|
||||
payload: {},
|
||||
},
|
||||
references: [
|
||||
{ id: '1', name: 'associated-cases', type: 'cases' },
|
||||
{ id: '3', name: 'connectorId', type: 'action' },
|
||||
],
|
||||
type: 'cases-user-actions',
|
||||
},
|
||||
{
|
||||
attributes: {
|
||||
action: 'delete',
|
||||
created_at: '2022-01-09T22:00:00.000Z',
|
||||
created_by: {
|
||||
email: 'elastic@elastic.co',
|
||||
full_name: 'Elastic User',
|
||||
username: 'elastic',
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "1",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
type: 'delete_case',
|
||||
owner: 'securitySolution',
|
||||
payload: {},
|
||||
"message": "User deleted case id: 1",
|
||||
},
|
||||
references: [
|
||||
{ id: '2', name: 'associated-cases', type: 'cases' },
|
||||
{ id: '4', name: 'connectorId', type: 'action' },
|
||||
],
|
||||
type: 'cases-user-actions',
|
||||
},
|
||||
],
|
||||
{ refresh: undefined }
|
||||
);
|
||||
],
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_action_delete_case",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"deletion",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "2",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "User deleted case id: 2",
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1073,6 +1237,181 @@ describe('CaseUserActionService', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('logs the correct user actions when bulk updating cases', async () => {
|
||||
await service.bulkCreateUpdateCase({
|
||||
...commonArgs,
|
||||
originalCases,
|
||||
updatedCases,
|
||||
user: commonArgs.user,
|
||||
});
|
||||
|
||||
expect(mockAuditLogger.log).toBeCalledTimes(8);
|
||||
expect(mockAuditLogger.log.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_action_update_case_title",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"change",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "1",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "User updated the title for case id: 1 - user action id: 0",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_action_update_case_status",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"change",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "1",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "User updated the status for case id: 1 - user action id: 1",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_action_update_case_connector",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"change",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "1",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "User changed the case connector to id: 456 for case id: 1 - user action id: 2",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_action_update_case_description",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"change",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "2",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "User updated the description for case id: 2 - user action id: 3",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_action_add_case_tags",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"change",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "2",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "User added tags to case id: 2 - user action id: 4",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_action_delete_case_tags",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"deletion",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "2",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "User deleted tags in case id: 2 - user action id: 5",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_action_update_case_settings",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"change",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "2",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "User updated the settings for case id: 2 - user action id: 6",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_action_update_case_severity",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"change",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "2",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "User updated the severity for case id: 2 - user action id: 7",
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('creates the correct user actions when an assignee is added', async () => {
|
||||
await service.bulkCreateUpdateCase({
|
||||
...commonArgs,
|
||||
|
@ -1120,6 +1459,41 @@ describe('CaseUserActionService', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
it('logs the correct user actions when an assignee is added', async () => {
|
||||
await service.bulkCreateUpdateCase({
|
||||
...commonArgs,
|
||||
originalCases,
|
||||
updatedCases: updatedAssigneesCases,
|
||||
user: commonArgs.user,
|
||||
});
|
||||
|
||||
expect(mockAuditLogger.log).toBeCalledTimes(1);
|
||||
expect(mockAuditLogger.log.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_action_add_case_assignees",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"change",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "1",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "User assigned uids: [1] to case id: 1 - user action id: 0",
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('creates the correct user actions when an assignee is removed', async () => {
|
||||
const casesWithAssigneeRemoved: Array<SavedObjectsUpdateResponse<CaseAttributes>> = [
|
||||
{
|
||||
|
@ -1177,6 +1551,51 @@ describe('CaseUserActionService', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
it('logs the correct user actions when an assignee is removed', async () => {
|
||||
const casesWithAssigneeRemoved: Array<SavedObjectsUpdateResponse<CaseAttributes>> = [
|
||||
{
|
||||
...createCaseSavedObjectResponse(),
|
||||
id: '1',
|
||||
attributes: {
|
||||
assignees: [],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
await service.bulkCreateUpdateCase({
|
||||
...commonArgs,
|
||||
originalCases: originalCasesWithAssignee,
|
||||
updatedCases: casesWithAssigneeRemoved,
|
||||
user: commonArgs.user,
|
||||
});
|
||||
|
||||
expect(mockAuditLogger.log).toBeCalledTimes(1);
|
||||
expect(mockAuditLogger.log.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_action_delete_case_assignees",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"deletion",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "1",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "User unassigned uids: [1] from case id: 1 - user action id: 0",
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('creates the correct user actions when assignees are added and removed', async () => {
|
||||
const caseAssignees: Array<SavedObjectsUpdateResponse<CaseAttributes>> = [
|
||||
{
|
||||
|
@ -1262,6 +1681,71 @@ describe('CaseUserActionService', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
it('logs the correct user actions when assignees are added and removed', async () => {
|
||||
const caseAssignees: Array<SavedObjectsUpdateResponse<CaseAttributes>> = [
|
||||
{
|
||||
...createCaseSavedObjectResponse(),
|
||||
id: '1',
|
||||
attributes: {
|
||||
assignees: [{ uid: '2' }],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
await service.bulkCreateUpdateCase({
|
||||
...commonArgs,
|
||||
originalCases: originalCasesWithAssignee,
|
||||
updatedCases: caseAssignees,
|
||||
user: commonArgs.user,
|
||||
});
|
||||
|
||||
expect(mockAuditLogger.log).toBeCalledTimes(2);
|
||||
expect(mockAuditLogger.log.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_action_add_case_assignees",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"change",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "1",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "User assigned uids: [2] to case id: 1 - user action id: 0",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_action_delete_case_assignees",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"deletion",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "1",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "User unassigned uids: [1] from case id: 1 - user action id: 1",
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('creates the correct user actions when tags are added and removed', async () => {
|
||||
await service.bulkCreateUpdateCase({
|
||||
...commonArgs,
|
||||
|
@ -1333,6 +1817,61 @@ describe('CaseUserActionService', () => {
|
|||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('logs the correct user actions when tags are added and removed', async () => {
|
||||
await service.bulkCreateUpdateCase({
|
||||
...commonArgs,
|
||||
originalCases,
|
||||
updatedCases: updatedTagsCases,
|
||||
user: commonArgs.user,
|
||||
});
|
||||
|
||||
expect(mockAuditLogger.log).toBeCalledTimes(2);
|
||||
expect(mockAuditLogger.log.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_action_add_case_tags",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"change",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "1",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "User added tags to case id: 1 - user action id: 0",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_action_delete_case_tags",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"deletion",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "1",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "User deleted tags in case id: 1 - user action id: 1",
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulkCreateAttachmentDeletion', () => {
|
||||
|
@ -1395,20 +1934,58 @@ describe('CaseUserActionService', () => {
|
|||
{ refresh: undefined }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('creates user actions', async () => {
|
||||
await service.create<{ title: string }>({
|
||||
unsecuredSavedObjectsClient,
|
||||
attributes: { title: 'test' },
|
||||
references: [],
|
||||
it('logs delete comment user action', async () => {
|
||||
await service.bulkCreateAttachmentDeletion({
|
||||
...commonArgs,
|
||||
attachments,
|
||||
});
|
||||
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith(
|
||||
'cases-user-actions',
|
||||
{ title: 'test' },
|
||||
{ references: [] }
|
||||
);
|
||||
|
||||
expect(mockAuditLogger.log).toBeCalledTimes(2);
|
||||
expect(mockAuditLogger.log.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_action_delete_comment",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"deletion",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "1",
|
||||
"type": "cases-comments",
|
||||
},
|
||||
},
|
||||
"message": "User deleted comment id: 1 for case id: 123 - user action id: 0",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_action_delete_comment",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"type": Array [
|
||||
"deletion",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "2",
|
||||
"type": "cases-comments",
|
||||
},
|
||||
},
|
||||
"message": "User deleted comment id: 2 for case id: 123 - user action id: 1",
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1458,7 +2035,6 @@ describe('CaseUserActionService', () => {
|
|||
|
||||
it('it returns an empty array if the response is not valid', async () => {
|
||||
const res = await service.getUniqueConnectors({
|
||||
unsecuredSavedObjectsClient,
|
||||
caseId: '123',
|
||||
});
|
||||
|
||||
|
@ -1472,7 +2048,6 @@ describe('CaseUserActionService', () => {
|
|||
} as unknown as Promise<SavedObjectsFindResponse>);
|
||||
|
||||
const res = await service.getUniqueConnectors({
|
||||
unsecuredSavedObjectsClient,
|
||||
caseId: '123',
|
||||
});
|
||||
|
||||
|
@ -1485,7 +2060,6 @@ describe('CaseUserActionService', () => {
|
|||
|
||||
it('it returns the unique connectors', async () => {
|
||||
await service.getUniqueConnectors({
|
||||
unsecuredSavedObjectsClient,
|
||||
caseId: '123',
|
||||
});
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import type {
|
|||
Logger,
|
||||
SavedObject,
|
||||
SavedObjectReference,
|
||||
SavedObjectsBulkResponse,
|
||||
SavedObjectsClientContract,
|
||||
SavedObjectsFindResponse,
|
||||
SavedObjectsUpdateResponse,
|
||||
|
@ -18,6 +19,7 @@ import type {
|
|||
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { KueryNode } from '@kbn/es-query';
|
||||
import type { AuditLogger } from '@kbn/security-plugin/server';
|
||||
import { isCommentRequestTypePersistableState } from '../../../common/utils/attachments';
|
||||
import {
|
||||
isConnectorUserAction,
|
||||
|
@ -27,7 +29,6 @@ import {
|
|||
isCommentUserAction,
|
||||
} from '../../../common/utils/user_actions';
|
||||
import type {
|
||||
ActionOperationValues,
|
||||
ActionTypeValues,
|
||||
CaseAttributes,
|
||||
CaseUserActionAttributes,
|
||||
|
@ -37,6 +38,7 @@ import type {
|
|||
CaseAssignees,
|
||||
CommentRequest,
|
||||
User,
|
||||
UserAction as Action,
|
||||
} from '../../../common/api';
|
||||
import { Actions, ActionTypes, NONE_CONNECTOR_ID } from '../../../common/api';
|
||||
import {
|
||||
|
@ -45,7 +47,6 @@ import {
|
|||
MAX_DOCS_PER_PAGE,
|
||||
CASE_COMMENT_SAVED_OBJECT,
|
||||
} from '../../../common/constants';
|
||||
import type { ClientArgs } from '..';
|
||||
import {
|
||||
CASE_REF_NAME,
|
||||
COMMENT_REF_NAME,
|
||||
|
@ -56,10 +57,11 @@ import {
|
|||
import { findConnectorIdReference } from '../transform';
|
||||
import { buildFilter, combineFilters, arraysDifference } from '../../client/utils';
|
||||
import type {
|
||||
Attributes,
|
||||
BuilderParameters,
|
||||
BuilderReturnValue,
|
||||
CommonArguments,
|
||||
CreateUserAction,
|
||||
UserActionEvent,
|
||||
UserActionParameters,
|
||||
} from './types';
|
||||
import { BuilderFactory } from './builder_factory';
|
||||
|
@ -69,31 +71,24 @@ import { injectPersistableReferencesToSO } from '../../attachment_framework/so_r
|
|||
import type { IndexRefresh } from '../types';
|
||||
import { isAssigneesArray, isStringArray } from './type_guards';
|
||||
import type { CaseSavedObject } from '../../common/types';
|
||||
|
||||
interface GetCaseUserActionArgs extends ClientArgs {
|
||||
caseId: string;
|
||||
}
|
||||
import { UserActionAuditLogger } from './audit_logger';
|
||||
import { createDeleteEvent } from './builders/delete_case';
|
||||
|
||||
export interface UserActionItem {
|
||||
attributes: CaseUserActionAttributesWithoutConnectorId;
|
||||
references: SavedObjectReference[];
|
||||
}
|
||||
|
||||
interface PostCaseUserActionArgs extends ClientArgs, IndexRefresh {
|
||||
actions: BuilderReturnValue[];
|
||||
interface PostCaseUserActionArgs extends IndexRefresh {
|
||||
actions: UserActionEvent[];
|
||||
}
|
||||
|
||||
interface CreateUserActionES<T> extends ClientArgs, IndexRefresh {
|
||||
interface CreateUserActionES<T> extends IndexRefresh {
|
||||
attributes: T;
|
||||
references: SavedObjectReference[];
|
||||
}
|
||||
|
||||
type CommonUserActionArgs = ClientArgs & CommonArguments;
|
||||
|
||||
interface BulkCreateCaseDeletionUserAction extends ClientArgs, IndexRefresh {
|
||||
cases: Array<{ id: string; owner: string; connectorId: string }>;
|
||||
user: User;
|
||||
}
|
||||
type CommonUserActionArgs = CommonArguments;
|
||||
|
||||
interface GetUserActionItemByDifference extends CommonUserActionArgs {
|
||||
field: string;
|
||||
|
@ -106,7 +101,7 @@ interface TypedUserActionDiffedItems<T> extends GetUserActionItemByDifference {
|
|||
newValue: T[];
|
||||
}
|
||||
|
||||
interface BulkCreateBulkUpdateCaseUserActions extends ClientArgs, IndexRefresh {
|
||||
interface BulkCreateBulkUpdateCaseUserActions extends IndexRefresh {
|
||||
originalCases: CaseSavedObject[];
|
||||
updatedCases: Array<SavedObjectsUpdateResponse<CaseAttributes>>;
|
||||
user: User;
|
||||
|
@ -127,20 +122,35 @@ type CreatePayloadFunction<Item, ActionType extends ActionTypeValues> = (
|
|||
export class CaseUserActionService {
|
||||
private static readonly userActionFieldsAllowed: Set<string> = new Set(Object.keys(ActionTypes));
|
||||
|
||||
private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract;
|
||||
private readonly builderFactory: BuilderFactory;
|
||||
private readonly persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry;
|
||||
private readonly log: Logger;
|
||||
private readonly auditLogger: UserActionAuditLogger;
|
||||
|
||||
constructor({
|
||||
log,
|
||||
persistableStateAttachmentTypeRegistry,
|
||||
unsecuredSavedObjectsClient,
|
||||
auditLogger,
|
||||
}: {
|
||||
log: Logger;
|
||||
persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry;
|
||||
unsecuredSavedObjectsClient: SavedObjectsClientContract;
|
||||
auditLogger: AuditLogger;
|
||||
}) {
|
||||
this.log = log;
|
||||
this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient;
|
||||
this.persistableStateAttachmentTypeRegistry = persistableStateAttachmentTypeRegistry;
|
||||
|
||||
constructor(
|
||||
private readonly log: Logger,
|
||||
private readonly persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry
|
||||
) {
|
||||
this.builderFactory = new BuilderFactory({
|
||||
persistableStateAttachmentTypeRegistry: this.persistableStateAttachmentTypeRegistry,
|
||||
});
|
||||
|
||||
this.auditLogger = new UserActionAuditLogger(auditLogger);
|
||||
}
|
||||
|
||||
private getUserActionItemByDifference(
|
||||
params: GetUserActionItemByDifference
|
||||
): BuilderReturnValue[] {
|
||||
private getUserActionItemByDifference(params: GetUserActionItemByDifference): UserActionEvent[] {
|
||||
const { field, originalValue, newValue, caseId, owner, user } = params;
|
||||
|
||||
if (!CaseUserActionService.userActionFieldsAllowed.has(field)) {
|
||||
|
@ -228,7 +238,7 @@ export class CaseUserActionService {
|
|||
}: {
|
||||
commonArgs: CommonUserActionArgs;
|
||||
actionType: ActionType;
|
||||
action: ActionOperationValues;
|
||||
action: Action;
|
||||
createPayload: CreatePayloadFunction<Item, ActionType>;
|
||||
modifiedItems?: Item[] | null;
|
||||
}) {
|
||||
|
@ -251,91 +261,60 @@ export class CaseUserActionService {
|
|||
return userAction;
|
||||
}
|
||||
|
||||
public async bulkCreateCaseDeletion({
|
||||
unsecuredSavedObjectsClient,
|
||||
cases,
|
||||
user,
|
||||
refresh,
|
||||
}: BulkCreateCaseDeletionUserAction): Promise<void> {
|
||||
this.log.debug(`Attempting to create a create case user action`);
|
||||
const userActionsWithReferences = cases.reduce<BuilderReturnValue[]>((acc, caseInfo) => {
|
||||
const userActionBuilder = this.builderFactory.getBuilder(ActionTypes.delete_case);
|
||||
const deleteCaseUserAction = userActionBuilder?.build({
|
||||
action: Actions.delete,
|
||||
caseId: caseInfo.id,
|
||||
user,
|
||||
owner: caseInfo.owner,
|
||||
connectorId: caseInfo.connectorId,
|
||||
payload: {},
|
||||
});
|
||||
public async bulkAuditLogCaseDeletion(caseIds: string[]) {
|
||||
this.log.debug(`Attempting to log bulk case deletion`);
|
||||
|
||||
if (deleteCaseUserAction == null) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return [...acc, deleteCaseUserAction];
|
||||
}, []);
|
||||
|
||||
await this.bulkCreate({
|
||||
unsecuredSavedObjectsClient,
|
||||
actions: userActionsWithReferences,
|
||||
refresh,
|
||||
});
|
||||
for (const id of caseIds) {
|
||||
this.auditLogger.log(createDeleteEvent({ caseId: id, action: Actions.delete }));
|
||||
}
|
||||
}
|
||||
|
||||
public async bulkCreateUpdateCase({
|
||||
unsecuredSavedObjectsClient,
|
||||
originalCases,
|
||||
updatedCases,
|
||||
user,
|
||||
refresh,
|
||||
}: BulkCreateBulkUpdateCaseUserActions): Promise<void> {
|
||||
const userActionsWithReferences = updatedCases.reduce<BuilderReturnValue[]>(
|
||||
(acc, updatedCase) => {
|
||||
const originalCase = originalCases.find(({ id }) => id === updatedCase.id);
|
||||
const builtUserActions = updatedCases.reduce<UserActionEvent[]>((acc, updatedCase) => {
|
||||
const originalCase = originalCases.find(({ id }) => id === updatedCase.id);
|
||||
|
||||
if (originalCase == null) {
|
||||
return acc;
|
||||
}
|
||||
if (originalCase == null) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
const caseId = updatedCase.id;
|
||||
const owner = originalCase.attributes.owner;
|
||||
const caseId = updatedCase.id;
|
||||
const owner = originalCase.attributes.owner;
|
||||
|
||||
const userActions: BuilderReturnValue[] = [];
|
||||
const updatedFields = Object.keys(updatedCase.attributes);
|
||||
const userActions: UserActionEvent[] = [];
|
||||
const updatedFields = Object.keys(updatedCase.attributes);
|
||||
|
||||
updatedFields
|
||||
.filter((field) => CaseUserActionService.userActionFieldsAllowed.has(field))
|
||||
.forEach((field) => {
|
||||
const originalValue = get(originalCase, ['attributes', field]);
|
||||
const newValue = get(updatedCase, ['attributes', field]);
|
||||
userActions.push(
|
||||
...this.getUserActionItemByDifference({
|
||||
unsecuredSavedObjectsClient,
|
||||
field,
|
||||
originalValue,
|
||||
newValue,
|
||||
user,
|
||||
owner,
|
||||
caseId,
|
||||
})
|
||||
);
|
||||
});
|
||||
updatedFields
|
||||
.filter((field) => CaseUserActionService.userActionFieldsAllowed.has(field))
|
||||
.forEach((field) => {
|
||||
const originalValue = get(originalCase, ['attributes', field]);
|
||||
const newValue = get(updatedCase, ['attributes', field]);
|
||||
userActions.push(
|
||||
...this.getUserActionItemByDifference({
|
||||
field,
|
||||
originalValue,
|
||||
newValue,
|
||||
user,
|
||||
owner,
|
||||
caseId,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
return [...acc, ...userActions];
|
||||
},
|
||||
[]
|
||||
);
|
||||
return [...acc, ...userActions];
|
||||
}, []);
|
||||
|
||||
await this.bulkCreate({
|
||||
unsecuredSavedObjectsClient,
|
||||
actions: userActionsWithReferences,
|
||||
await this.bulkCreateAndLog({
|
||||
userActions: builtUserActions,
|
||||
refresh,
|
||||
});
|
||||
}
|
||||
|
||||
private async bulkCreateAttachment({
|
||||
unsecuredSavedObjectsClient,
|
||||
caseId,
|
||||
attachments,
|
||||
user,
|
||||
|
@ -343,43 +322,37 @@ export class CaseUserActionService {
|
|||
refresh,
|
||||
}: BulkCreateAttachmentUserAction): Promise<void> {
|
||||
this.log.debug(`Attempting to create a bulk create case user action`);
|
||||
const userActionsWithReferences = attachments.reduce<BuilderReturnValue[]>(
|
||||
(acc, attachment) => {
|
||||
const userActionBuilder = this.builderFactory.getBuilder(ActionTypes.comment);
|
||||
const commentUserAction = userActionBuilder?.build({
|
||||
action,
|
||||
caseId,
|
||||
user,
|
||||
owner: attachment.owner,
|
||||
attachmentId: attachment.id,
|
||||
payload: { attachment: attachment.attachment },
|
||||
});
|
||||
const userActions = attachments.reduce<UserActionEvent[]>((acc, attachment) => {
|
||||
const userActionBuilder = this.builderFactory.getBuilder(ActionTypes.comment);
|
||||
const commentUserAction = userActionBuilder?.build({
|
||||
action,
|
||||
caseId,
|
||||
user,
|
||||
owner: attachment.owner,
|
||||
attachmentId: attachment.id,
|
||||
payload: { attachment: attachment.attachment },
|
||||
});
|
||||
|
||||
if (commentUserAction == null) {
|
||||
return acc;
|
||||
}
|
||||
if (commentUserAction == null) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return [...acc, commentUserAction];
|
||||
},
|
||||
[]
|
||||
);
|
||||
return [...acc, commentUserAction];
|
||||
}, []);
|
||||
|
||||
await this.bulkCreate({
|
||||
unsecuredSavedObjectsClient,
|
||||
actions: userActionsWithReferences,
|
||||
await this.bulkCreateAndLog({
|
||||
userActions,
|
||||
refresh,
|
||||
});
|
||||
}
|
||||
|
||||
public async bulkCreateAttachmentDeletion({
|
||||
unsecuredSavedObjectsClient,
|
||||
caseId,
|
||||
attachments,
|
||||
user,
|
||||
refresh,
|
||||
}: BulkCreateAttachmentUserAction): Promise<void> {
|
||||
await this.bulkCreateAttachment({
|
||||
unsecuredSavedObjectsClient,
|
||||
caseId,
|
||||
attachments,
|
||||
user,
|
||||
|
@ -389,14 +362,12 @@ export class CaseUserActionService {
|
|||
}
|
||||
|
||||
public async bulkCreateAttachmentCreation({
|
||||
unsecuredSavedObjectsClient,
|
||||
caseId,
|
||||
attachments,
|
||||
user,
|
||||
refresh,
|
||||
}: BulkCreateAttachmentUserAction): Promise<void> {
|
||||
await this.bulkCreateAttachment({
|
||||
unsecuredSavedObjectsClient,
|
||||
caseId,
|
||||
attachments,
|
||||
user,
|
||||
|
@ -406,7 +377,6 @@ export class CaseUserActionService {
|
|||
}
|
||||
|
||||
public async createUserAction<T extends keyof BuilderParameters>({
|
||||
unsecuredSavedObjectsClient,
|
||||
action,
|
||||
type,
|
||||
caseId,
|
||||
|
@ -432,8 +402,7 @@ export class CaseUserActionService {
|
|||
});
|
||||
|
||||
if (userAction) {
|
||||
const { attributes, references } = userAction;
|
||||
await this.create({ unsecuredSavedObjectsClient, attributes, references, refresh });
|
||||
await this.createAndLog({ userAction, refresh });
|
||||
}
|
||||
} catch (error) {
|
||||
this.log.error(`Error on creating user action of type: ${type}. Error: ${error}`);
|
||||
|
@ -441,16 +410,13 @@ export class CaseUserActionService {
|
|||
}
|
||||
}
|
||||
|
||||
public async getAll({
|
||||
unsecuredSavedObjectsClient,
|
||||
caseId,
|
||||
}: GetCaseUserActionArgs): Promise<SavedObjectsFindResponse<CaseUserActionResponse>> {
|
||||
public async getAll(caseId: string): Promise<SavedObjectsFindResponse<CaseUserActionResponse>> {
|
||||
try {
|
||||
const id = caseId;
|
||||
const type = CASE_SAVED_OBJECT;
|
||||
|
||||
const userActions =
|
||||
await unsecuredSavedObjectsClient.find<CaseUserActionAttributesWithoutConnectorId>({
|
||||
await this.unsecuredSavedObjectsClient.find<CaseUserActionAttributesWithoutConnectorId>({
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
hasReference: { type, id },
|
||||
page: 1,
|
||||
|
@ -469,30 +435,87 @@ export class CaseUserActionService {
|
|||
}
|
||||
}
|
||||
|
||||
public async create<T>({
|
||||
unsecuredSavedObjectsClient,
|
||||
public async getUserActionIdsForCases(caseIds: string[]) {
|
||||
try {
|
||||
this.log.debug(`Attempting to retrieve user actions associated with cases: [${caseIds}]`);
|
||||
|
||||
const finder = this.unsecuredSavedObjectsClient.createPointInTimeFinder({
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
hasReference: caseIds.map((id) => ({ id, type: CASE_SAVED_OBJECT })),
|
||||
sortField: 'created_at',
|
||||
sortOrder: 'asc',
|
||||
/**
|
||||
* We only care about the ids so to reduce the data returned we should limit the fields in the response. Core
|
||||
* doesn't support retrieving no fields (id would always be returned anyway) so to limit it we'll only request
|
||||
* the owner even though we don't need it.
|
||||
*/
|
||||
fields: ['owner'],
|
||||
perPage: MAX_DOCS_PER_PAGE,
|
||||
});
|
||||
|
||||
const ids: string[] = [];
|
||||
for await (const userActionSavedObject of finder.find()) {
|
||||
ids.push(...userActionSavedObject.saved_objects.map((userAction) => userAction.id));
|
||||
}
|
||||
|
||||
return ids;
|
||||
} catch (error) {
|
||||
this.log.error(`Error retrieving user action ids for cases: [${caseIds}]: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async createAndLog({
|
||||
userAction,
|
||||
refresh,
|
||||
}: {
|
||||
userAction: UserActionEvent;
|
||||
} & IndexRefresh): Promise<void> {
|
||||
const createdUserAction = await this.create({ ...userAction.parameters, refresh });
|
||||
this.auditLogger.log(userAction.eventDetails, createdUserAction.id);
|
||||
}
|
||||
|
||||
private async create<T>({
|
||||
attributes,
|
||||
references,
|
||||
refresh,
|
||||
}: CreateUserActionES<T>): Promise<void> {
|
||||
}: CreateUserActionES<T>): Promise<SavedObject<T>> {
|
||||
try {
|
||||
this.log.debug(`Attempting to POST a new case user action`);
|
||||
|
||||
await unsecuredSavedObjectsClient.create<T>(CASE_USER_ACTION_SAVED_OBJECT, attributes, {
|
||||
references: references ?? [],
|
||||
refresh,
|
||||
});
|
||||
return await this.unsecuredSavedObjectsClient.create<T>(
|
||||
CASE_USER_ACTION_SAVED_OBJECT,
|
||||
attributes,
|
||||
{
|
||||
references: references ?? [],
|
||||
refresh,
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
this.log.error(`Error on POST a new case user action: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async bulkCreate({
|
||||
unsecuredSavedObjectsClient,
|
||||
private async bulkCreateAndLog({
|
||||
userActions,
|
||||
refresh,
|
||||
}: { userActions: UserActionEvent[] } & IndexRefresh) {
|
||||
const createdUserActions = await this.bulkCreate({ actions: userActions, refresh });
|
||||
|
||||
if (!createdUserActions) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < userActions.length; i++) {
|
||||
this.auditLogger.log(userActions[i].eventDetails, createdUserActions.saved_objects[i].id);
|
||||
}
|
||||
}
|
||||
|
||||
private async bulkCreate({
|
||||
actions,
|
||||
refresh,
|
||||
}: PostCaseUserActionArgs): Promise<void> {
|
||||
}: PostCaseUserActionArgs): Promise<SavedObjectsBulkResponse<Attributes> | undefined> {
|
||||
if (isEmpty(actions)) {
|
||||
return;
|
||||
}
|
||||
|
@ -500,8 +523,11 @@ export class CaseUserActionService {
|
|||
try {
|
||||
this.log.debug(`Attempting to POST a new case user action`);
|
||||
|
||||
await unsecuredSavedObjectsClient.bulkCreate(
|
||||
actions.map((action) => ({ type: CASE_USER_ACTION_SAVED_OBJECT, ...action })),
|
||||
return await this.unsecuredSavedObjectsClient.bulkCreate(
|
||||
actions.map((action) => ({
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
...action.parameters,
|
||||
})),
|
||||
{ refresh }
|
||||
);
|
||||
} catch (error) {
|
||||
|
@ -511,11 +537,9 @@ export class CaseUserActionService {
|
|||
}
|
||||
|
||||
public async findStatusChanges({
|
||||
unsecuredSavedObjectsClient,
|
||||
caseId,
|
||||
filter,
|
||||
}: {
|
||||
unsecuredSavedObjectsClient: SavedObjectsClientContract;
|
||||
caseId: string;
|
||||
filter?: KueryNode;
|
||||
}): Promise<Array<SavedObject<CaseUserActionResponse>>> {
|
||||
|
@ -539,7 +563,7 @@ export class CaseUserActionService {
|
|||
const combinedFilters = combineFilters([updateActionFilter, statusChangeFilter, filter]);
|
||||
|
||||
const finder =
|
||||
unsecuredSavedObjectsClient.createPointInTimeFinder<CaseUserActionAttributesWithoutConnectorId>(
|
||||
this.unsecuredSavedObjectsClient.createPointInTimeFinder<CaseUserActionAttributesWithoutConnectorId>(
|
||||
{
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
hasReference: { type: CASE_SAVED_OBJECT, id: caseId },
|
||||
|
@ -569,10 +593,8 @@ export class CaseUserActionService {
|
|||
public async getUniqueConnectors({
|
||||
caseId,
|
||||
filter,
|
||||
unsecuredSavedObjectsClient,
|
||||
}: {
|
||||
caseId: string;
|
||||
unsecuredSavedObjectsClient: SavedObjectsClientContract;
|
||||
filter?: KueryNode;
|
||||
}): Promise<Array<{ id: string }>> {
|
||||
try {
|
||||
|
@ -586,7 +608,7 @@ export class CaseUserActionService {
|
|||
|
||||
const combinedFilter = combineFilters([connectorsFilter, filter]);
|
||||
|
||||
const response = await unsecuredSavedObjectsClient.find<
|
||||
const response = await this.unsecuredSavedObjectsClient.find<
|
||||
CaseUserActionAttributesWithoutConnectorId,
|
||||
{ references: { connectors: { ids: { buckets: Array<{ key: string }> } } } }
|
||||
>({
|
||||
|
|
|
@ -98,11 +98,24 @@ export interface Attributes {
|
|||
payload: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface BuilderReturnValue {
|
||||
export interface SavedObjectParameters {
|
||||
attributes: Attributes;
|
||||
references: SavedObjectReference[];
|
||||
}
|
||||
|
||||
export interface EventDetails {
|
||||
getMessage: (storedUserActionId?: string) => string;
|
||||
action: UserAction;
|
||||
descriptiveAction: string;
|
||||
savedObjectId: string;
|
||||
savedObjectType: string;
|
||||
}
|
||||
|
||||
export interface UserActionEvent {
|
||||
parameters: SavedObjectParameters;
|
||||
eventDetails: EventDetails;
|
||||
}
|
||||
|
||||
export type CommonBuilderArguments = CommonArguments & {
|
||||
action: UserAction;
|
||||
type: UserActionTypes;
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
import { defaultUser, getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock';
|
||||
import { getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock';
|
||||
import {
|
||||
deleteCasesByESQuery,
|
||||
deleteCasesUserActions,
|
||||
|
@ -17,7 +17,6 @@ import {
|
|||
deleteCases,
|
||||
createComment,
|
||||
getComment,
|
||||
removeServerGeneratedPropertiesFromUserAction,
|
||||
getCase,
|
||||
superUserSpace1Auth,
|
||||
getCaseUserActions,
|
||||
|
@ -53,7 +52,22 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
expect(body).to.eql({});
|
||||
});
|
||||
|
||||
it(`should delete a case's comments when that case gets deleted`, async () => {
|
||||
it('should delete multiple cases and their user actions', async () => {
|
||||
const [case1, case2] = await Promise.all([
|
||||
createCase(supertest, getPostCaseRequest()),
|
||||
createCase(supertest, getPostCaseRequest()),
|
||||
]);
|
||||
|
||||
await deleteCases({ supertest, caseIDs: [case1.id, case2.id] });
|
||||
|
||||
const userActionsCase1 = await getCaseUserActions({ supertest, caseID: case1.id });
|
||||
expect(userActionsCase1.length).to.be(0);
|
||||
|
||||
const userActionsCase2 = await getCaseUserActions({ supertest, caseID: case2.id });
|
||||
expect(userActionsCase2.length).to.be(0);
|
||||
});
|
||||
|
||||
it(`should delete a case's comments and user actions when that case gets deleted`, async () => {
|
||||
const postedCase = await createCase(supertest, getPostCaseRequest());
|
||||
const patchedCase = await createComment({
|
||||
supertest,
|
||||
|
@ -76,23 +90,16 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
commentId: patchedCase.comments![0].id,
|
||||
expectedHttpCode: 404,
|
||||
});
|
||||
|
||||
const userActions = await getCaseUserActions({ supertest, caseID: postedCase.id });
|
||||
expect(userActions.length).to.be(0);
|
||||
});
|
||||
|
||||
it('should create a user action when deleting a case', async () => {
|
||||
it('should delete all user actions when deleting a case', async () => {
|
||||
const postedCase = await createCase(supertest, getPostCaseRequest());
|
||||
await deleteCases({ supertest, caseIDs: [postedCase.id] });
|
||||
const userActions = await getCaseUserActions({ supertest, caseID: postedCase.id });
|
||||
const creationUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]);
|
||||
|
||||
expect(creationUserAction).to.eql({
|
||||
action: 'delete',
|
||||
type: 'delete_case',
|
||||
created_by: defaultUser,
|
||||
case_id: postedCase.id,
|
||||
comment_id: null,
|
||||
payload: {},
|
||||
owner: 'securitySolutionFixture',
|
||||
});
|
||||
expect(userActions.length).to.be(0);
|
||||
});
|
||||
|
||||
it('unhappy path - 404s when case is not there', async () => {
|
||||
|
|
|
@ -68,19 +68,12 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
expect(createCaseUserAction.payload.connector).to.eql(postCaseReq.connector);
|
||||
});
|
||||
|
||||
it('creates a delete case user action when a case is deleted', async () => {
|
||||
it('deletes all user actions when a case is deleted', async () => {
|
||||
const theCase = await createCase(supertest, postCaseReq);
|
||||
await deleteCases({ supertest, caseIDs: [theCase.id] });
|
||||
const userActions = await getCaseUserActions({ supertest, caseID: theCase.id });
|
||||
|
||||
const userAction = userActions[1];
|
||||
|
||||
// One for creation and one for deletion
|
||||
expect(userActions.length).to.eql(2);
|
||||
|
||||
expect(userAction.action).to.eql('delete');
|
||||
expect(userAction.type).to.eql('delete_case');
|
||||
expect(userAction.payload).to.eql({});
|
||||
expect(userActions.length).to.be(0);
|
||||
});
|
||||
|
||||
it('creates a status update user action when changing the status', async () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue