From daad20da4451bd0d3928120d15a6b02fc3880749 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 26 Oct 2020 22:56:01 +0200 Subject: [PATCH 01/23] Init connector --- .../plugins/case/common/api/cases/comment.ts | 1 + .../case/server/connectors/case/index.ts | 103 ++++++++++++++++++ .../case/server/connectors/case/schema.ts | 53 +++++++++ .../server/connectors/case/translations.ts | 11 ++ .../case/server/connectors/case/types.ts | 26 +++++ .../plugins/case/server/connectors/index.ts | 55 ++++++++++ x-pack/plugins/case/server/plugin.ts | 11 ++ .../server/saved_object_types/comments.ts | 3 + 8 files changed, 263 insertions(+) create mode 100644 x-pack/plugins/case/server/connectors/case/index.ts create mode 100644 x-pack/plugins/case/server/connectors/case/schema.ts create mode 100644 x-pack/plugins/case/server/connectors/case/translations.ts create mode 100644 x-pack/plugins/case/server/connectors/case/types.ts create mode 100644 x-pack/plugins/case/server/connectors/index.ts diff --git a/x-pack/plugins/case/common/api/cases/comment.ts b/x-pack/plugins/case/common/api/cases/comment.ts index 4549b1c31a7cf..c2861262e4d88 100644 --- a/x-pack/plugins/case/common/api/cases/comment.ts +++ b/x-pack/plugins/case/common/api/cases/comment.ts @@ -10,6 +10,7 @@ import { UserRT } from '../user'; const CommentBasicRt = rt.type({ comment: rt.string, + type: rt.union([rt.string, rt.null, rt.undefined]), }); export const CommentAttributesRt = rt.intersection([ diff --git a/x-pack/plugins/case/server/connectors/case/index.ts b/x-pack/plugins/case/server/connectors/case/index.ts new file mode 100644 index 0000000000000..ba37d3788d766 --- /dev/null +++ b/x-pack/plugins/case/server/connectors/case/index.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { curry } from 'lodash'; + +import { KibanaRequest } from 'kibana/server'; +import { ActionTypeExecutorResult } from '../../../../actions/common'; +import { ActionType, ActionTypeExecutorOptions } from '../../../../actions/server'; +import { CasePatchRequest, ConnectorTypes } from '../../../common/api'; +import { createCaseClient } from '../../client'; +import { CaseExecutorParamsSchema, CaseConfigurationSchema } from './schema'; +import { + CaseExecutorParams, + CaseExecutorResponse, + ExecutorSubActionCreateParams, + ExecutorSubActionUpdateParams, + ExecutorSubActionAddCommentParams, +} from './types'; +import * as i18n from './translations'; + +import { GetActionTypeParams } from '..'; + +const supportedSubActions: string[] = ['create', 'update', 'addComment']; + +// action type definition +export function getActionType({ + logger, + caseService, + caseConfigureService, + userActionService, +}: GetActionTypeParams): ActionType<{}, {}, CaseExecutorParams, CaseExecutorResponse | {}> { + return { + id: '.case', + minimumLicenseRequired: 'platinum', + name: i18n.NAME, + validate: { + config: CaseConfigurationSchema, + params: CaseExecutorParamsSchema, + }, + executor: curry(executor)({ logger, caseService, caseConfigureService, userActionService }), + }; +} + +// action executor + +async function executor( + { logger, caseService, caseConfigureService, userActionService }: GetActionTypeParams, + execOptions: ActionTypeExecutorOptions<{}, {}, CaseExecutorParams> +): Promise> { + const { actionId, params, services } = execOptions; + const { subAction, subActionParams } = params as CaseExecutorParams; + let data: CaseExecutorResponse | null = null; + + const { savedObjectsClient } = services; + const caseClient = createCaseClient({ + savedObjectsClient, + request: {} as KibanaRequest, + caseService, + caseConfigureService, + userActionService, + }); + + if (!supportedSubActions.includes(subAction)) { + const errorMessage = `[Action][Case] subAction ${subAction} not implemented.`; + logger.error(errorMessage); + throw new Error(errorMessage); + } + + if (subAction === 'create') { + const createParams = subActionParams as ExecutorSubActionCreateParams; + const theCase = { + ...createParams, + connector: { id: 'none', name: 'none', type: ConnectorTypes.none, fields: null }, + }; + + data = await caseClient.create({ theCase }); + } + + if (subAction === 'update') { + const updateParams = subActionParams as ExecutorSubActionUpdateParams; + const updateParamsWithoutNullValues = Object.entries(updateParams).reduce( + (acc, [key, value]) => ({ + ...acc, + ...(value != null ? { [key]: value } : {}), + }), + {} as CasePatchRequest + ); + + data = await caseClient.update({ cases: { cases: [updateParamsWithoutNullValues] } }); + } + + if (subAction === 'addComment') { + const addCommentParams = subActionParams as ExecutorSubActionAddCommentParams; + const { caseId, comment } = addCommentParams; + + data = await caseClient.addComment({ caseId, comment }); + } + + return { status: 'ok', data: data ?? {}, actionId }; +} diff --git a/x-pack/plugins/case/server/connectors/case/schema.ts b/x-pack/plugins/case/server/connectors/case/schema.ts new file mode 100644 index 0000000000000..15ae7321207fb --- /dev/null +++ b/x-pack/plugins/case/server/connectors/case/schema.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; + +// Reserved for future implementation +export const CaseConfigurationSchema = schema.object({}); + +const CommentProps = { + comment: schema.string(), + type: schema.nullable(schema.string()), +}; + +const CaseBasicProps = { + description: schema.string(), + title: schema.string(), + tags: schema.arrayOf(schema.string()), +}; + +const CaseUpdateRequestProps = { + id: schema.string(), + version: schema.string(), + description: schema.nullable(CaseBasicProps.description), + title: schema.nullable(CaseBasicProps.title), + tags: schema.nullable(CaseBasicProps.tags), + status: schema.nullable(schema.string()), +}; + +const CaseAddCommentRequestProps = { + caseId: schema.string(), + comment: schema.object(CommentProps), +}; + +export const ExecutorSubActionCreateParamsSchema = schema.object(CaseBasicProps); +export const ExecutorSubActionUpdateParamsSchema = schema.object(CaseUpdateRequestProps); +export const ExecutorSubActionAddCommentParamsSchema = schema.object(CaseAddCommentRequestProps); + +export const CaseExecutorParamsSchema = schema.oneOf([ + schema.object({ + subAction: schema.literal('create'), + subActionParams: ExecutorSubActionCreateParamsSchema, + }), + schema.object({ + subAction: schema.literal('update'), + subActionParams: ExecutorSubActionUpdateParamsSchema, + }), + schema.object({ + subAction: schema.literal('addComment'), + subActionParams: ExecutorSubActionAddCommentParamsSchema, + }), +]); diff --git a/x-pack/plugins/case/server/connectors/case/translations.ts b/x-pack/plugins/case/server/connectors/case/translations.ts new file mode 100644 index 0000000000000..6a3c173bb5d9d --- /dev/null +++ b/x-pack/plugins/case/server/connectors/case/translations.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const NAME = i18n.translate('xpack.case.actions.caseTitle', { + defaultMessage: 'Case', +}); diff --git a/x-pack/plugins/case/server/connectors/case/types.ts b/x-pack/plugins/case/server/connectors/case/types.ts new file mode 100644 index 0000000000000..16377d5b3fc27 --- /dev/null +++ b/x-pack/plugins/case/server/connectors/case/types.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TypeOf } from '@kbn/config-schema'; +import { + CaseExecutorParamsSchema, + ExecutorSubActionCreateParamsSchema, + ExecutorSubActionUpdateParamsSchema, + CaseConfigurationSchema, + ExecutorSubActionAddCommentParamsSchema, +} from './schema'; +import { CaseResponse, CasesResponse } from '../../../common/api'; + +export type CaseConfiguration = TypeOf; + +export type ExecutorSubActionCreateParams = TypeOf; +export type ExecutorSubActionUpdateParams = TypeOf; +export type ExecutorSubActionAddCommentParams = TypeOf< + typeof ExecutorSubActionAddCommentParamsSchema +>; + +export type CaseExecutorParams = TypeOf; +export type CaseExecutorResponse = CaseResponse | CasesResponse; diff --git a/x-pack/plugins/case/server/connectors/index.ts b/x-pack/plugins/case/server/connectors/index.ts new file mode 100644 index 0000000000000..425ea3f8b0037 --- /dev/null +++ b/x-pack/plugins/case/server/connectors/index.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Logger } from 'kibana/server'; +import { + ActionTypeConfig, + ActionTypeSecrets, + ActionTypeParams, + ActionType, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../actions/server/types'; +import { + CaseServiceSetup, + CaseConfigureServiceSetup, + CaseUserActionServiceSetup, +} from '../services'; + +import { getActionType as getCaseConnector } from './case'; + +export interface GetActionTypeParams { + logger: Logger; + caseService: CaseServiceSetup; + caseConfigureService: CaseConfigureServiceSetup; + userActionService: CaseUserActionServiceSetup; +} + +export interface RegisterConnectorsArgs extends GetActionTypeParams { + actionsRegisterType< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams + >( + actionType: ActionType + ): void; +} + +export const registerConnectors = ({ + actionsRegisterType, + logger, + caseService, + caseConfigureService, + userActionService, +}: RegisterConnectorsArgs) => { + actionsRegisterType( + getCaseConnector({ + logger, + caseService, + caseConfigureService, + userActionService, + }) + ); +}; diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index 5398f8ed0ae83..64c4b422d1cf7 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -15,6 +15,7 @@ import { import { CoreSetup, CoreStart } from 'src/core/server'; import { SecurityPluginSetup } from '../../security/server'; +import { PluginSetupContract as ActionsPluginSetup } from '../../actions/server'; import { APP_ID } from '../common/constants'; import { ConfigType } from './config'; @@ -34,6 +35,7 @@ import { CaseUserActionServiceSetup, } from './services'; import { createCaseClient } from './client'; +import { registerConnectors } from './connectors'; function createConfig$(context: PluginInitializerContext) { return context.config.create().pipe(map((config) => config)); @@ -41,6 +43,7 @@ function createConfig$(context: PluginInitializerContext) { export interface PluginsSetup { security: SecurityPluginSetup; + actions: ActionsPluginSetup; } export class CasePlugin { @@ -94,6 +97,14 @@ export class CasePlugin { userActionService: this.userActionService, router, }); + + registerConnectors({ + actionsRegisterType: plugins.actions.registerType, + logger: this.log, + caseService: this.caseService, + caseConfigureService: this.caseConfigureService, + userActionService: this.userActionService, + }); } public async start(core: CoreStart) { diff --git a/x-pack/plugins/case/server/saved_object_types/comments.ts b/x-pack/plugins/case/server/saved_object_types/comments.ts index 8b69f272d5b0d..cb6f6ca84b1fd 100644 --- a/x-pack/plugins/case/server/saved_object_types/comments.ts +++ b/x-pack/plugins/case/server/saved_object_types/comments.ts @@ -17,6 +17,9 @@ export const caseCommentSavedObjectType: SavedObjectsType = { comment: { type: 'text', }, + type: { + type: 'keyword', + }, created_at: { type: 'date', }, From 167ffa9c923883a23a88763ed530b1ca6a50be6d Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 27 Oct 2020 13:19:39 +0200 Subject: [PATCH 02/23] Add test --- .../case/server/connectors/case/index.test.ts | 364 ++++++++++++++++++ .../case/server/connectors/case/index.ts | 7 +- .../case/server/connectors/case/types.ts | 14 + 3 files changed, 382 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/case/server/connectors/case/index.test.ts diff --git a/x-pack/plugins/case/server/connectors/case/index.test.ts b/x-pack/plugins/case/server/connectors/case/index.test.ts new file mode 100644 index 0000000000000..b734e1f69175a --- /dev/null +++ b/x-pack/plugins/case/server/connectors/case/index.test.ts @@ -0,0 +1,364 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Logger } from '../../../../../../src/core/server'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { actionsMock } from '../../../../actions/server/mocks'; +import { validateParams } from '../../../../actions/server/lib'; +import { ConnectorTypes } from '../../../common/api'; +import { + createCaseServiceMock, + createConfigureServiceMock, + createUserActionServiceMock, +} from '../../services/mocks'; +import { CaseActionType, CaseActionTypeExecutorOptions, CaseExecutorParams } from './types'; +import { getActionType } from '.'; +import { createCaseClientMock } from '../../client/mocks'; + +const mockCaseClient = createCaseClientMock(); + +jest.mock('../../client', () => ({ + createCaseClient: () => mockCaseClient, +})); + +const services = actionsMock.createServices(); +let caseActionType: CaseActionType; + +describe('case connector', () => { + beforeEach(() => { + jest.resetAllMocks(); + const logger = loggingSystemMock.create().get() as jest.Mocked; + const caseService = createCaseServiceMock(); + const caseConfigureService = createConfigureServiceMock(); + const userActionService = createUserActionServiceMock(); + caseActionType = getActionType({ + logger, + caseService, + caseConfigureService, + userActionService, + }); + }); + + describe('params validation', () => { + describe('create', () => { + it('succeeds when params is valid', () => { + const params: Record = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + tags: ['case', 'connector'], + description: 'Yo fields!!', + }, + }; + + expect(validateParams(caseActionType, params)).toEqual(params); + }); + + it('fails when params is not valid', () => { + const params: Record = { + subAction: 'create', + }; + + expect(() => { + validateParams(caseActionType, params); + }).toThrow(); + }); + }); + + describe('update', () => { + it('succeeds when params is valid', () => { + const params: Record = { + subAction: 'update', + subActionParams: { + id: 'case-id', + version: '123', + title: 'Update title', + }, + }; + + expect(validateParams(caseActionType, params)).toEqual({ + ...params, + subActionParams: { + description: null, + tags: null, + title: null, + status: null, + ...(params.subActionParams as Record), + }, + }); + }); + + it('fails when params is not valid', () => { + const params: Record = { + subAction: 'update', + }; + + expect(() => { + validateParams(caseActionType, params); + }).toThrow(); + }); + }); + + describe('add comment', () => { + it('succeeds when params is valid', () => { + const params: Record = { + subAction: 'addComment', + subActionParams: { + caseId: 'case-id', + comment: { comment: 'a comment', type: 'normal' }, + }, + }; + + expect(validateParams(caseActionType, params)).toEqual(params); + }); + + it('fails when params is not valid', () => { + const params: Record = { + subAction: 'addComment', + }; + + expect(() => { + validateParams(caseActionType, params); + }).toThrow(); + }); + }); + }); + + describe('execute', () => { + it('allows only supported sub-actions', async () => { + expect.assertions(2); + const actionId = 'some-id'; + const params: CaseExecutorParams = { + // @ts-expect-error + subAction: 'not-supported', + // @ts-expect-error + subActionParams: {}, + }; + + const executorOptions: CaseActionTypeExecutorOptions = { + actionId, + config: {}, + params, + secrets: {}, + services, + }; + + caseActionType.executor(executorOptions).catch((e) => { + expect(e).not.toBeNull(); + expect(e.message).toBe('[Action][Case] subAction not-supported not implemented.'); + }); + }); + + describe('create', () => { + it('executes correctly', async () => { + const createReturn = { + id: 'mock-it', + comments: [], + totalComment: 0, + closed_at: null, + closed_by: null, + connector: { id: 'none', name: 'none', type: ConnectorTypes.none, fields: null }, + created_at: '2019-11-25T21:54:48.952Z', + created_by: { + full_name: 'Awesome D00d', + email: 'd00d@awesome.com', + username: 'awesome', + }, + title: 'Case from case connector!!', + tags: ['case', 'connector'], + description: 'Yo fields!!', + external_service: null, + status: 'open' as const, + updated_at: null, + updated_by: null, + version: 'WzksMV0=', + }; + + mockCaseClient.create.mockReturnValue(Promise.resolve(createReturn)); + + const actionId = 'some-id'; + const params: CaseExecutorParams = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + tags: ['case', 'connector'], + description: 'Yo fields!!', + }, + }; + + const executorOptions: CaseActionTypeExecutorOptions = { + actionId, + config: {}, + params, + secrets: {}, + services, + }; + + const result = await caseActionType.executor(executorOptions); + + expect(result).toEqual({ actionId, status: 'ok', data: createReturn }); + expect(mockCaseClient.create).toHaveBeenCalledWith({ + theCase: { + ...params.subActionParams, + connector: { + fields: null, + id: 'none', + name: 'none', + type: '.none', + }, + }, + }); + }); + }); + + describe('update', () => { + it('executes correctly', async () => { + const updateReturn = [ + { + closed_at: '2019-11-25T21:54:48.952Z', + closed_by: { + email: 'd00d@awesome.com', + full_name: 'Awesome D00d', + username: 'awesome', + }, + comments: [], + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + created_at: '2019-11-25T21:54:48.952Z', + created_by: { + email: 'testemail@elastic.co', + full_name: 'elastic', + username: 'elastic', + }, + description: 'This is a brand new case of a bad meanie defacing data', + id: 'mock-id-1', + external_service: null, + status: 'open' as const, + tags: ['defacement'], + title: 'Update title', + totalComment: 0, + updated_at: '2019-11-25T21:54:48.952Z', + updated_by: { + email: 'd00d@awesome.com', + full_name: 'Awesome D00d', + username: 'awesome', + }, + version: 'WzE3LDFd', + }, + ]; + + mockCaseClient.update.mockReturnValue(Promise.resolve(updateReturn)); + + const actionId = 'some-id'; + const params: CaseExecutorParams = { + subAction: 'update', + subActionParams: { + id: 'case-id', + version: '123', + title: 'Update title', + description: null, + tags: null, + status: null, + }, + }; + + const executorOptions: CaseActionTypeExecutorOptions = { + actionId, + config: {}, + params, + secrets: {}, + services, + }; + + const result = await caseActionType.executor(executorOptions); + + expect(result).toEqual({ actionId, status: 'ok', data: updateReturn }); + expect(mockCaseClient.update).toHaveBeenCalledWith({ + // Null values have been striped out. + cases: { + cases: [ + { + id: 'case-id', + version: '123', + title: 'Update title', + }, + ], + }, + }); + }); + }); + + describe('addComment', () => { + it('executes correctly', async () => { + const commentReturn = { + id: 'mock-it', + totalComment: 0, + closed_at: null, + closed_by: null, + connector: { id: 'none', name: 'none', type: ConnectorTypes.none, fields: null }, + created_at: '2019-11-25T21:54:48.952Z', + created_by: { full_name: 'Awesome D00d', email: 'd00d@awesome.com', username: 'awesome' }, + description: 'This is a brand new case of a bad meanie defacing data', + external_service: null, + title: 'Super Bad Security Issue', + status: 'open' as const, + tags: ['defacement'], + updated_at: null, + updated_by: null, + version: 'WzksMV0=', + comments: [ + { + comment: 'a comment', + created_at: '2020-10-23T21:54:48.952Z', + created_by: { + email: 'd00d@awesome.com', + full_name: 'Awesome D00d', + username: 'awesome', + }, + id: 'mock-comment', + pushed_at: null, + pushed_by: null, + updated_at: null, + updated_by: null, + version: 'WzksMV0=', + }, + ], + }; + + mockCaseClient.addComment.mockReturnValue(Promise.resolve(commentReturn)); + + const actionId = 'some-id'; + const params: CaseExecutorParams = { + subAction: 'addComment', + subActionParams: { + caseId: 'case-id', + comment: { comment: 'a comment', type: 'normal' }, + }, + }; + + const executorOptions: CaseActionTypeExecutorOptions = { + actionId, + config: {}, + params, + secrets: {}, + services, + }; + + const result = await caseActionType.executor(executorOptions); + + expect(result).toEqual({ actionId, status: 'ok', data: commentReturn }); + expect(mockCaseClient.addComment).toHaveBeenCalledWith({ + caseId: 'case-id', + comment: { comment: 'a comment', type: 'normal' }, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/case/server/connectors/case/index.ts b/x-pack/plugins/case/server/connectors/case/index.ts index ba37d3788d766..277b028d38773 100644 --- a/x-pack/plugins/case/server/connectors/case/index.ts +++ b/x-pack/plugins/case/server/connectors/case/index.ts @@ -8,7 +8,6 @@ import { curry } from 'lodash'; import { KibanaRequest } from 'kibana/server'; import { ActionTypeExecutorResult } from '../../../../actions/common'; -import { ActionType, ActionTypeExecutorOptions } from '../../../../actions/server'; import { CasePatchRequest, ConnectorTypes } from '../../../common/api'; import { createCaseClient } from '../../client'; import { CaseExecutorParamsSchema, CaseConfigurationSchema } from './schema'; @@ -18,6 +17,8 @@ import { ExecutorSubActionCreateParams, ExecutorSubActionUpdateParams, ExecutorSubActionAddCommentParams, + CaseActionType, + CaseActionTypeExecutorOptions, } from './types'; import * as i18n from './translations'; @@ -31,7 +32,7 @@ export function getActionType({ caseService, caseConfigureService, userActionService, -}: GetActionTypeParams): ActionType<{}, {}, CaseExecutorParams, CaseExecutorResponse | {}> { +}: GetActionTypeParams): CaseActionType { return { id: '.case', minimumLicenseRequired: 'platinum', @@ -48,7 +49,7 @@ export function getActionType({ async function executor( { logger, caseService, caseConfigureService, userActionService }: GetActionTypeParams, - execOptions: ActionTypeExecutorOptions<{}, {}, CaseExecutorParams> + execOptions: CaseActionTypeExecutorOptions ): Promise> { const { actionId, params, services } = execOptions; const { subAction, subActionParams } = params as CaseExecutorParams; diff --git a/x-pack/plugins/case/server/connectors/case/types.ts b/x-pack/plugins/case/server/connectors/case/types.ts index 16377d5b3fc27..394c2bfac2047 100644 --- a/x-pack/plugins/case/server/connectors/case/types.ts +++ b/x-pack/plugins/case/server/connectors/case/types.ts @@ -5,6 +5,7 @@ */ import { TypeOf } from '@kbn/config-schema'; +import { ActionType, ActionTypeExecutorOptions } from '../../../../actions/server'; import { CaseExecutorParamsSchema, ExecutorSubActionCreateParamsSchema, @@ -24,3 +25,16 @@ export type ExecutorSubActionAddCommentParams = TypeOf< export type CaseExecutorParams = TypeOf; export type CaseExecutorResponse = CaseResponse | CasesResponse; + +export type CaseActionType = ActionType< + CaseConfiguration, + {}, + CaseExecutorParams, + CaseExecutorResponse | {} +>; + +export type CaseActionTypeExecutorOptions = ActionTypeExecutorOptions< + CaseConfiguration, + {}, + CaseExecutorParams +>; From 0938b3b50e2ce1b6a13b63df8b43e2f6af402c01 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 27 Oct 2020 13:37:02 +0200 Subject: [PATCH 03/23] Improve comment type --- x-pack/plugins/case/common/api/cases/comment.ts | 17 +++++++++++++---- .../case/server/connectors/case/schema.ts | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/case/common/api/cases/comment.ts b/x-pack/plugins/case/common/api/cases/comment.ts index c2861262e4d88..8642e9b3e8ffa 100644 --- a/x-pack/plugins/case/common/api/cases/comment.ts +++ b/x-pack/plugins/case/common/api/cases/comment.ts @@ -8,11 +8,16 @@ import * as rt from 'io-ts'; import { UserRT } from '../user'; -const CommentBasicRt = rt.type({ +const CommentRequiredFieldsRt = rt.type({ comment: rt.string, - type: rt.union([rt.string, rt.null, rt.undefined]), }); +const CommentOptionalFieldsRt = rt.partial({ + type: rt.union([rt.literal('alert'), rt.literal('normal'), rt.null, rt.undefined]), +}); + +const CommentBasicRt = rt.intersection([CommentRequiredFieldsRt, CommentOptionalFieldsRt]); + export const CommentAttributesRt = rt.intersection([ CommentBasicRt, rt.type({ @@ -25,7 +30,10 @@ export const CommentAttributesRt = rt.intersection([ }), ]); -export const CommentRequestRt = CommentBasicRt; +export const CommentRequestRt = rt.type({ + ...CommentOptionalFieldsRt.props, + ...CommentRequiredFieldsRt.props, +}); export const CommentResponseRt = rt.intersection([ CommentAttributesRt, @@ -38,7 +46,8 @@ export const CommentResponseRt = rt.intersection([ export const AllCommentsResponseRT = rt.array(CommentResponseRt); export const CommentPatchRequestRt = rt.intersection([ - rt.partial(CommentRequestRt.props), + rt.partial(CommentRequiredFieldsRt.props), + CommentOptionalFieldsRt, rt.type({ id: rt.string, version: rt.string }), ]); diff --git a/x-pack/plugins/case/server/connectors/case/schema.ts b/x-pack/plugins/case/server/connectors/case/schema.ts index 15ae7321207fb..e85c3852da6f7 100644 --- a/x-pack/plugins/case/server/connectors/case/schema.ts +++ b/x-pack/plugins/case/server/connectors/case/schema.ts @@ -10,7 +10,7 @@ export const CaseConfigurationSchema = schema.object({}); const CommentProps = { comment: schema.string(), - type: schema.nullable(schema.string()), + type: schema.nullable(schema.oneOf([schema.literal('alert'), schema.literal('normal')])), }; const CaseBasicProps = { From 8f836cab070972feb783a7a0b1ff97ae798fcbe0 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 27 Oct 2020 15:09:07 +0200 Subject: [PATCH 04/23] Add integration tests --- .../basic/tests/connectors/case.ts | 458 ++++++++++++++++++ .../case_api_integration/basic/tests/index.ts | 1 + .../case_api_integration/common/config.ts | 1 + .../case_api_integration/common/lib/mock.ts | 18 +- 4 files changed, 476 insertions(+), 2 deletions(-) create mode 100644 x-pack/test/case_api_integration/basic/tests/connectors/case.ts diff --git a/x-pack/test/case_api_integration/basic/tests/connectors/case.ts b/x-pack/test/case_api_integration/basic/tests/connectors/case.ts new file mode 100644 index 0000000000000..50fc1473e83d7 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/connectors/case.ts @@ -0,0 +1,458 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../plugins/case/common/constants'; +import { + postCaseReq, + postCaseResp, + removeServerGeneratedPropertiesFromCase, + removeServerGeneratedPropertiesFromComments, +} from '../../../common/lib/mock'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + + describe('case_connector', () => { + let createdActionId = ''; + + it('should return 200 when creating a case action successfully', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + + expect(createdAction).to.eql({ + id: createdActionId, + isPreconfigured: false, + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }); + + const { body: fetchedAction } = await supertest + .get(`/api/actions/action/${createdActionId}`) + .expect(200); + + expect(fetchedAction).to.eql({ + id: fetchedAction.id, + isPreconfigured: false, + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }); + }); + + describe('create', () => { + it('should respond with a 400 Bad Request when creating a case without title', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'create', + subActionParams: { + tags: ['case', 'connector'], + description: 'case description', + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/action/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subActionParams.title]: expected value of type [string] but got [undefined]\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should respond with a 400 Bad Request when creating a case without description', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + tags: ['case', 'connector'], + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/action/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subActionParams.description]: expected value of type [string] but got [undefined]\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should respond with a 400 Bad Request when creating a case without tags', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + description: 'case description', + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/action/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subActionParams.tags]: expected value of type [array] but got [undefined]\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should create a case', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + tags: ['case', 'connector'], + description: 'case description', + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/action/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${caseConnector.body.data.id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const data = removeServerGeneratedPropertiesFromCase(body); + expect(data).to.eql({ + ...postCaseResp(caseConnector.body.data.id), + ...params.subActionParams, + created_by: { + email: null, + full_name: null, + username: null, + }, + }); + }); + }); + + describe('update', () => { + it('should respond with a 400 Bad Request when updating a case without id', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'update', + subActionParams: { + version: '123', + title: 'Case from case connector!!', + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/action/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subActionParams.id]: expected value of type [string] but got [undefined]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should respond with a 400 Bad Request when updating a case without version', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'update', + subActionParams: { + id: '123', + title: 'Case from case connector!!', + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/action/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subActionParams.version]: expected value of type [string] but got [undefined]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should update a case', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + + const caseRes = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const params = { + subAction: 'update', + subActionParams: { + id: caseRes.body.id, + version: caseRes.body.version, + title: 'Case from case connector!!', + }, + }; + + await supertest + .post(`/api/actions/action/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${caseRes.body.id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const data = removeServerGeneratedPropertiesFromCase(body); + expect(data).to.eql({ + ...postCaseResp(caseRes.body.id), + title: 'Case from case connector!!', + updated_by: { + email: null, + full_name: null, + username: null, + }, + }); + }); + }); + + describe('addComment', () => { + it('should respond with a 400 Bad Request when adding a comment to a case without caseId', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'update', + subActionParams: { + comment: { comment: 'a comment' }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/action/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subActionParams.id]: expected value of type [string] but got [undefined]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should respond with a 400 Bad Request when adding a comment to a case without comment', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'update', + subActionParams: { + caseId: '123', + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/action/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subActionParams.id]: expected value of type [string] but got [undefined]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should add a comment', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + + const caseRes = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const params = { + subAction: 'addComment', + subActionParams: { + caseId: caseRes.body.id, + comment: { comment: 'a comment' }, + }, + }; + + await supertest + .post(`/api/actions/action/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${caseRes.body.id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const data = removeServerGeneratedPropertiesFromCase(body); + const comments = removeServerGeneratedPropertiesFromComments(data.comments ?? []); + expect({ ...data, comments }).to.eql({ + ...postCaseResp(caseRes.body.id), + comments, + totalComment: 1, + updated_by: { + email: null, + full_name: null, + username: null, + }, + }); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/index.ts b/x-pack/test/case_api_integration/basic/tests/index.ts index aaf2338cde2f0..2f7af95e264f8 100644 --- a/x-pack/test/case_api_integration/basic/tests/index.ts +++ b/x-pack/test/case_api_integration/basic/tests/index.ts @@ -31,6 +31,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./configure/get_connectors')); loadTestFile(require.resolve('./configure/patch_configure')); loadTestFile(require.resolve('./configure/post_configure')); + loadTestFile(require.resolve('./connectors/case')); // Migrations loadTestFile(require.resolve('./cases/migrations')); diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts index 72d1bc4ec9a37..86d69266c6ec6 100644 --- a/x-pack/test/case_api_integration/common/config.ts +++ b/x-pack/test/case_api_integration/common/config.ts @@ -26,6 +26,7 @@ const enabledActionTypes = [ '.servicenow', '.slack', '.webhook', + '.case', 'test.authorization', 'test.failing', 'test.index-record', diff --git a/x-pack/test/case_api_integration/common/lib/mock.ts b/x-pack/test/case_api_integration/common/lib/mock.ts index 18c57ad3b0b69..72e1639143eae 100644 --- a/x-pack/test/case_api_integration/common/lib/mock.ts +++ b/x-pack/test/case_api_integration/common/lib/mock.ts @@ -8,6 +8,7 @@ import { CasePostRequest, CaseResponse, CasesFindResponse, + CommentResponse, ConnectorTypes, } from '../../../../plugins/case/common/api'; export const defaultUser = { email: null, full_name: null, username: 'elastic' }; @@ -27,8 +28,11 @@ export const postCommentReq: { comment: string } = { comment: 'This is a cool comment', }; -export const postCaseResp = (id: string): Partial => ({ - ...postCaseReq, +export const postCaseResp = ( + id: string, + req: CasePostRequest = postCaseReq +): Partial => ({ + ...req, id, comments: [], totalComment: 0, @@ -47,6 +51,16 @@ export const removeServerGeneratedPropertiesFromCase = ( return rest; }; +export const removeServerGeneratedPropertiesFromComments = ( + comments: CommentResponse[] +): Array> => { + return comments.map((comment) => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { created_at, updated_at, version, ...rest } = comment; + return rest; + }); +}; + export const findCasesResp: CasesFindResponse = { page: 1, per_page: 20, From ae576538b5b29fdf75337d06ab6229e71140b843 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 27 Oct 2020 15:11:03 +0200 Subject: [PATCH 05/23] Fix i18n --- x-pack/plugins/case/server/connectors/case/index.ts | 3 +-- .../case/server/connectors/case/translations.ts | 11 ----------- 2 files changed, 1 insertion(+), 13 deletions(-) delete mode 100644 x-pack/plugins/case/server/connectors/case/translations.ts diff --git a/x-pack/plugins/case/server/connectors/case/index.ts b/x-pack/plugins/case/server/connectors/case/index.ts index 277b028d38773..12c4487f9c70d 100644 --- a/x-pack/plugins/case/server/connectors/case/index.ts +++ b/x-pack/plugins/case/server/connectors/case/index.ts @@ -20,7 +20,6 @@ import { CaseActionType, CaseActionTypeExecutorOptions, } from './types'; -import * as i18n from './translations'; import { GetActionTypeParams } from '..'; @@ -36,7 +35,7 @@ export function getActionType({ return { id: '.case', minimumLicenseRequired: 'platinum', - name: i18n.NAME, + name: 'Case', validate: { config: CaseConfigurationSchema, params: CaseExecutorParamsSchema, diff --git a/x-pack/plugins/case/server/connectors/case/translations.ts b/x-pack/plugins/case/server/connectors/case/translations.ts deleted file mode 100644 index 6a3c173bb5d9d..0000000000000 --- a/x-pack/plugins/case/server/connectors/case/translations.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const NAME = i18n.translate('xpack.case.actions.caseTitle', { - defaultMessage: 'Case', -}); From 79765d81008ddb2c0e63d53f9bf0f54ce748ffa5 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 29 Oct 2020 11:00:32 +0200 Subject: [PATCH 06/23] Improve tests --- .../case/server/client/cases/create.test.ts | 54 ++++++++++++----- .../case/server/client/cases/update.test.ts | 58 +++++++++++-------- 2 files changed, 76 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/case/server/client/cases/create.test.ts b/x-pack/plugins/case/server/client/cases/create.test.ts index f253dd9f4feb4..d82979de2cb44 100644 --- a/x-pack/plugins/case/server/client/cases/create.test.ts +++ b/x-pack/plugins/case/server/client/cases/create.test.ts @@ -180,7 +180,7 @@ describe('create', () => { describe('unhappy path', () => { test('it throws when missing title', async () => { - expect.assertions(1); + expect.assertions(3); const postCase = { description: 'This is a brand new case of a bad meanie defacing data', tags: ['defacement'], @@ -199,11 +199,15 @@ describe('create', () => { caseClient.client // @ts-expect-error .create({ theCase: postCase }) - .catch((e) => expect(e).not.toBeNull()); + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }); }); test('it throws when missing description', async () => { - expect.assertions(1); + expect.assertions(3); const postCase = { title: 'a title', tags: ['defacement'], @@ -222,11 +226,15 @@ describe('create', () => { caseClient.client // @ts-expect-error .create({ theCase: postCase }) - .catch((e) => expect(e).not.toBeNull()); + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }); }); test('it throws when missing tags', async () => { - expect.assertions(1); + expect.assertions(3); const postCase = { title: 'a title', description: 'This is a brand new case of a bad meanie defacing data', @@ -245,11 +253,15 @@ describe('create', () => { caseClient.client // @ts-expect-error .create({ theCase: postCase }) - .catch((e) => expect(e).not.toBeNull()); + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }); }); test('it throws when missing connector ', async () => { - expect.assertions(1); + expect.assertions(3); const postCase = { title: 'a title', description: 'This is a brand new case of a bad meanie defacing data', @@ -263,11 +275,15 @@ describe('create', () => { caseClient.client // @ts-expect-error .create({ theCase: postCase }) - .catch((e) => expect(e).not.toBeNull()); + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }); }); test('it throws when connector missing the right fields', async () => { - expect.assertions(1); + expect.assertions(3); const postCase = { title: 'a title', description: 'This is a brand new case of a bad meanie defacing data', @@ -287,11 +303,15 @@ describe('create', () => { caseClient.client // @ts-expect-error .create({ theCase: postCase }) - .catch((e) => expect(e).not.toBeNull()); + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }); }); test('it throws if you passing status for a new case', async () => { - expect.assertions(1); + expect.assertions(3); const postCase = { title: 'a title', description: 'This is a brand new case of a bad meanie defacing data', @@ -309,7 +329,11 @@ describe('create', () => { caseSavedObject: mockCases, }); const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); - caseClient.client.create({ theCase: postCase }).catch((e) => expect(e).not.toBeNull()); + caseClient.client.create({ theCase: postCase }).catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }); }); it(`Returns an error if postNewCase throws`, async () => { @@ -329,7 +353,11 @@ describe('create', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); - caseClient.client.create({ theCase: postCase }).catch((e) => expect(e).not.toBeNull()); + caseClient.client.create({ theCase: postCase }).catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }); }); }); }); diff --git a/x-pack/plugins/case/server/client/cases/update.test.ts b/x-pack/plugins/case/server/client/cases/update.test.ts index 62d897999c11a..10eebd1210a9e 100644 --- a/x-pack/plugins/case/server/client/cases/update.test.ts +++ b/x-pack/plugins/case/server/client/cases/update.test.ts @@ -247,7 +247,7 @@ describe('update', () => { describe('unhappy path', () => { test('it throws when missing id', async () => { - expect.assertions(1); + expect.assertions(3); const patchCases = { cases: [ { @@ -270,11 +270,15 @@ describe('update', () => { caseClient.client // @ts-expect-error .update({ cases: patchCases }) - .catch((e) => expect(e).not.toBeNull()); + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }); }); test('it throws when missing version', async () => { - expect.assertions(1); + expect.assertions(3); const patchCases = { cases: [ { @@ -297,11 +301,15 @@ describe('update', () => { caseClient.client // @ts-expect-error .update({ cases: patchCases }) - .catch((e) => expect(e).not.toBeNull()); + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }); }); test('it throws when fields are identical', async () => { - expect.assertions(1); + expect.assertions(4); const patchCases = { cases: [ { @@ -317,14 +325,16 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); - caseClient.client - .update({ cases: patchCases }) - .catch((e) => - expect(e.message).toBe('All update fields are identical to current version.') - ); + caseClient.client.update({ cases: patchCases }).catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(406); + expect(e.message).toBe('All update fields are identical to current version.'); + }); }); test('it throws when case does not exist', async () => { + expect.assertions(4); const patchCases = { cases: [ { @@ -345,17 +355,18 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); - caseClient.client - .update({ cases: patchCases }) - .catch((e) => - expect(e.message).toBe( - 'These cases not-exists do not exist. Please check you have the correct ids.' - ) + caseClient.client.update({ cases: patchCases }).catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(404); + expect(e.message).toBe( + 'These cases not-exists do not exist. Please check you have the correct ids.' ); + }); }); test('it throws when cases conflicts', async () => { - expect.assertions(1); + expect.assertions(4); const patchCases = { cases: [ { @@ -371,13 +382,14 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); - caseClient.client - .update({ cases: patchCases }) - .catch((e) => - expect(e.message).toBe( - 'These cases mock-id-1 has been updated. Please refresh before saving additional updates.' - ) + caseClient.client.update({ cases: patchCases }).catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(409); + expect(e.message).toBe( + 'These cases mock-id-1 has been updated. Please refresh before saving additional updates.' ); + }); }); }); }); From 20fbfb8e183733449289eda71b912cb0ca279fa9 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 29 Oct 2020 16:21:08 +0200 Subject: [PATCH 07/23] Show unknown when username is null --- .../components/user_action_tree/helpers.tsx | 4 ++-- .../components/user_action_tree/index.tsx | 18 ++++++------------ .../user_action_tree/user_action_avatar.tsx | 16 +++++----------- .../user_action_tree/user_action_username.tsx | 11 +++++++---- .../user_action_username_with_avatar.tsx | 7 ++++--- 5 files changed, 24 insertions(+), 32 deletions(-) diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.tsx index 0ced285f9dcd9..2abcb70d676ef 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.tsx @@ -131,8 +131,8 @@ export const getUpdateAction = ({ }): EuiCommentProps => ({ username: ( ), type: 'update', diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx index 1967402fd81e0..de3e9c07ae8a3 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx @@ -217,8 +217,8 @@ export const UserActionTree = React.memo( () => ({ username: ( ), event: i18n.ADDED_DESCRIPTION, @@ -270,8 +270,8 @@ export const UserActionTree = React.memo( { username: ( ), 'data-test-subj': `comment-create-action-${comment.id}`, @@ -418,17 +418,11 @@ export const UserActionTree = React.memo( const bottomActions = [ { username: ( - + ), 'data-test-subj': 'add-comment', timelineIcon: ( - + ), className: 'isEdit', children: MarkdownNewComment, diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_avatar.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_avatar.tsx index 8339d9bedd123..025cbcb2e2710 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_avatar.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_avatar.tsx @@ -5,7 +5,9 @@ */ import React, { memo } from 'react'; -import { EuiAvatar, EuiLoadingSpinner } from '@elastic/eui'; +import { EuiAvatar } from '@elastic/eui'; + +import * as i18n from './translations'; interface UserActionAvatarProps { username?: string | null; @@ -13,17 +15,9 @@ interface UserActionAvatarProps { } const UserActionAvatarComponent = ({ username, fullName }: UserActionAvatarProps) => { - const avatarName = fullName && fullName.length > 0 ? fullName : username ?? null; + const avatarName = fullName && fullName.length > 0 ? fullName : username ?? i18n.UNKNOWN; - return ( - <> - {avatarName ? ( - - ) : ( - - )} - - ); + return ; }; export const UserActionAvatar = memo(UserActionAvatarComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username.tsx index dbc153ddbe577..8730de39ba39c 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username.tsx @@ -8,19 +8,22 @@ import React, { memo } from 'react'; import { EuiToolTip } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; +import * as i18n from './translations'; + interface UserActionUsernameProps { - username: string; - fullName?: string; + username?: string | null; + fullName?: string | null; } const UserActionUsernameComponent = ({ username, fullName }: UserActionUsernameProps) => { + const tooltipContent = (isEmpty(fullName) ? username : fullName) ?? i18n.UNKNOWN; return ( {isEmpty(fullName) ? username : fullName}

} + content={

{tooltipContent}

} data-test-subj="user-action-username-tooltip" > - {username} + {username ?? i18n.UNKNOWN.toLowerCase()}
); }; diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username_with_avatar.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username_with_avatar.tsx index e2326a3580e6f..9d5ab2d7ae6ef 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username_with_avatar.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username_with_avatar.tsx @@ -9,10 +9,11 @@ import { EuiFlexGroup, EuiFlexItem, EuiAvatar } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import { UserActionUsername } from './user_action_username'; +import * as i18n from './translations'; interface UserActionUsernameWithAvatarProps { - username: string; - fullName?: string; + username?: string | null; + fullName?: string | null; } const UserActionUsernameWithAvatarComponent = ({ @@ -29,7 +30,7 @@ const UserActionUsernameWithAvatarComponent = ({ From 985192f92bebeb4bc9eb124f1ce79fa0ac78416a Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 29 Oct 2020 18:30:47 +0200 Subject: [PATCH 08/23] Improve comment type --- .../plugins/case/common/api/cases/comment.ts | 17 ++------ .../case/server/client/comments/add.test.ts | 35 ++++++++++++++--- .../case/server/connectors/case/index.test.ts | 7 ++-- .../case/server/connectors/case/schema.ts | 2 +- .../api/__fixtures__/mock_saved_objects.ts | 3 ++ .../api/cases/comments/post_comment.test.ts | 4 ++ .../case/server/routes/api/utils.test.ts | 6 +++ .../plugins/case/server/routes/api/utils.ts | 3 ++ .../server/saved_object_types/comments.ts | 2 + .../server/saved_object_types/migrations.ts | 24 ++++++++++++ .../components/add_comment/index.test.tsx | 4 +- .../cases/components/add_comment/index.tsx | 3 +- .../user_action_avatar.test.tsx | 14 ++----- .../public/cases/containers/api.test.tsx | 1 + .../public/cases/containers/mock.ts | 1 + .../public/cases/containers/types.ts | 1 + .../containers/use_post_comment.test.tsx | 1 + .../basic/tests/connectors/case.ts | 39 ++++++++++++++++++- 18 files changed, 129 insertions(+), 38 deletions(-) diff --git a/x-pack/plugins/case/common/api/cases/comment.ts b/x-pack/plugins/case/common/api/cases/comment.ts index 8642e9b3e8ffa..2bc319bcb40b5 100644 --- a/x-pack/plugins/case/common/api/cases/comment.ts +++ b/x-pack/plugins/case/common/api/cases/comment.ts @@ -8,16 +8,11 @@ import * as rt from 'io-ts'; import { UserRT } from '../user'; -const CommentRequiredFieldsRt = rt.type({ +const CommentBasicRt = rt.type({ comment: rt.string, + type: rt.union([rt.literal('alert'), rt.literal('user')]), }); -const CommentOptionalFieldsRt = rt.partial({ - type: rt.union([rt.literal('alert'), rt.literal('normal'), rt.null, rt.undefined]), -}); - -const CommentBasicRt = rt.intersection([CommentRequiredFieldsRt, CommentOptionalFieldsRt]); - export const CommentAttributesRt = rt.intersection([ CommentBasicRt, rt.type({ @@ -30,10 +25,7 @@ export const CommentAttributesRt = rt.intersection([ }), ]); -export const CommentRequestRt = rt.type({ - ...CommentOptionalFieldsRt.props, - ...CommentRequiredFieldsRt.props, -}); +export const CommentRequestRt = CommentBasicRt; export const CommentResponseRt = rt.intersection([ CommentAttributesRt, @@ -46,8 +38,7 @@ export const CommentResponseRt = rt.intersection([ export const AllCommentsResponseRT = rt.array(CommentResponseRt); export const CommentPatchRequestRt = rt.intersection([ - rt.partial(CommentRequiredFieldsRt.props), - CommentOptionalFieldsRt, + rt.partial(CommentBasicRt.props), rt.type({ id: rt.string, version: rt.string }), ]); diff --git a/x-pack/plugins/case/server/client/comments/add.test.ts b/x-pack/plugins/case/server/client/comments/add.test.ts index 8a316740e41e0..49be8920744c0 100644 --- a/x-pack/plugins/case/server/client/comments/add.test.ts +++ b/x-pack/plugins/case/server/client/comments/add.test.ts @@ -30,13 +30,14 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); const res = await caseClient.client.addComment({ caseId: 'mock-id-1', - comment: { comment: 'Wow, good luck catching that bad meanie!' }, + comment: { comment: 'Wow, good luck catching that bad meanie!', type: 'user' }, }); expect(res.id).toEqual('mock-id-1'); expect(res.totalComment).toEqual(res.comments!.length); expect(res.comments![res.comments!.length - 1]).toEqual({ comment: 'Wow, good luck catching that bad meanie!', + type: 'user', created_at: '2020-10-23T21:54:48.952Z', created_by: { email: 'd00d@awesome.com', @@ -61,7 +62,7 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); const res = await caseClient.client.addComment({ caseId: 'mock-id-1', - comment: { comment: 'Wow, good luck catching that bad meanie!' }, + comment: { comment: 'Wow, good luck catching that bad meanie!', type: 'user' }, }); expect(res.updated_at).toEqual('2020-10-23T21:54:48.952Z'); @@ -81,7 +82,7 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); await caseClient.client.addComment({ caseId: 'mock-id-1', - comment: { comment: 'Wow, good luck catching that bad meanie!' }, + comment: { comment: 'Wow, good luck catching that bad meanie!', type: 'user' }, }); expect( @@ -125,12 +126,13 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient, true); const res = await caseClient.client.addComment({ caseId: 'mock-id-1', - comment: { comment: 'Wow, good luck catching that bad meanie!' }, + comment: { comment: 'Wow, good luck catching that bad meanie!', type: 'user' }, }); expect(res.id).toEqual('mock-id-1'); expect(res.comments![res.comments!.length - 1]).toEqual({ comment: 'Wow, good luck catching that bad meanie!', + type: 'user', created_at: '2020-10-23T21:54:48.952Z', created_by: { email: null, @@ -169,6 +171,27 @@ describe('addComment', () => { }); }); + test('it throws when missing comment type', async () => { + expect.assertions(3); + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + .addComment({ + caseId: 'mock-id-1', + // @ts-expect-error + comment: { comment: 'a comment' }, + }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }); + }); + test('it throws when the case does not exists', async () => { expect.assertions(3); @@ -180,7 +203,7 @@ describe('addComment', () => { caseClient.client .addComment({ caseId: 'not-exists', - comment: { comment: 'Wow, good luck catching that bad meanie!' }, + comment: { comment: 'Wow, good luck catching that bad meanie!', type: 'user' }, }) .catch((e) => { expect(e).not.toBeNull(); @@ -200,7 +223,7 @@ describe('addComment', () => { caseClient.client .addComment({ caseId: 'mock-id-1', - comment: { comment: 'Throw an error' }, + comment: { comment: 'Throw an error', type: 'user' }, }) .catch((e) => { expect(e).not.toBeNull(); diff --git a/x-pack/plugins/case/server/connectors/case/index.test.ts b/x-pack/plugins/case/server/connectors/case/index.test.ts index b734e1f69175a..e741f33e5df53 100644 --- a/x-pack/plugins/case/server/connectors/case/index.test.ts +++ b/x-pack/plugins/case/server/connectors/case/index.test.ts @@ -108,7 +108,7 @@ describe('case connector', () => { subAction: 'addComment', subActionParams: { caseId: 'case-id', - comment: { comment: 'a comment', type: 'normal' }, + comment: { comment: 'a comment', type: 'user' }, }, }; @@ -316,6 +316,7 @@ describe('case connector', () => { comments: [ { comment: 'a comment', + type: 'user' as const, created_at: '2020-10-23T21:54:48.952Z', created_by: { email: 'd00d@awesome.com', @@ -339,7 +340,7 @@ describe('case connector', () => { subAction: 'addComment', subActionParams: { caseId: 'case-id', - comment: { comment: 'a comment', type: 'normal' }, + comment: { comment: 'a comment', type: 'user' }, }, }; @@ -356,7 +357,7 @@ describe('case connector', () => { expect(result).toEqual({ actionId, status: 'ok', data: commentReturn }); expect(mockCaseClient.addComment).toHaveBeenCalledWith({ caseId: 'case-id', - comment: { comment: 'a comment', type: 'normal' }, + comment: { comment: 'a comment', type: 'user' }, }); }); }); diff --git a/x-pack/plugins/case/server/connectors/case/schema.ts b/x-pack/plugins/case/server/connectors/case/schema.ts index e85c3852da6f7..b49bb4a4a0c10 100644 --- a/x-pack/plugins/case/server/connectors/case/schema.ts +++ b/x-pack/plugins/case/server/connectors/case/schema.ts @@ -10,7 +10,7 @@ export const CaseConfigurationSchema = schema.object({}); const CommentProps = { comment: schema.string(), - type: schema.nullable(schema.oneOf([schema.literal('alert'), schema.literal('normal')])), + type: schema.oneOf([schema.literal('alert'), schema.literal('user')]), }; const CaseBasicProps = { diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts index e7ea381da9955..aef78c4f92cdb 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -207,6 +207,7 @@ export const mockCaseComments: Array> = [ id: 'mock-comment-1', attributes: { comment: 'Wow, good luck catching that bad meanie!', + type: 'user', created_at: '2019-11-25T21:55:00.177Z', created_by: { full_name: 'elastic', @@ -237,6 +238,7 @@ export const mockCaseComments: Array> = [ id: 'mock-comment-2', attributes: { comment: 'Well I decided to update my comment. So what? Deal with it.', + type: 'user', created_at: '2019-11-25T21:55:14.633Z', created_by: { full_name: 'elastic', @@ -268,6 +270,7 @@ export const mockCaseComments: Array> = [ id: 'mock-comment-3', attributes: { comment: 'Wow, good luck catching that bad meanie!', + type: 'user', created_at: '2019-11-25T22:32:30.608Z', created_by: { full_name: 'elastic', diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts index acc23815e3a39..784d9f9796456 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts @@ -36,6 +36,7 @@ describe('POST comment', () => { }, body: { comment: 'Wow, good luck catching that bad meanie!', + type: 'user', }, }); @@ -62,6 +63,7 @@ describe('POST comment', () => { }, body: { comment: 'Wow, good luck catching that bad meanie!', + type: 'user', }, }); @@ -112,6 +114,7 @@ describe('POST comment', () => { }, body: { comment: 'Wow, good luck catching that bad meanie!', + type: 'user', }, }); @@ -127,6 +130,7 @@ describe('POST comment', () => { expect(response.status).toEqual(200); expect(response.payload.comments[response.payload.comments.length - 1]).toEqual({ comment: 'Wow, good luck catching that bad meanie!', + type: 'user', created_at: '2019-11-25T21:54:48.952Z', created_by: { email: null, diff --git a/x-pack/plugins/case/server/routes/api/utils.test.ts b/x-pack/plugins/case/server/routes/api/utils.test.ts index 00584a9d7431f..3b03f9490f9c5 100644 --- a/x-pack/plugins/case/server/routes/api/utils.test.ts +++ b/x-pack/plugins/case/server/routes/api/utils.test.ts @@ -117,6 +117,7 @@ describe('Utils', () => { it('transforms correctly', () => { const comment = { comment: 'A comment', + type: 'user' as const, createdDate: '2020-04-09T09:43:51.778Z', email: 'elastic@elastic.co', full_name: 'Elastic', @@ -126,6 +127,7 @@ describe('Utils', () => { const res = transformNewComment(comment); expect(res).toEqual({ comment: 'A comment', + type: 'user', created_at: '2020-04-09T09:43:51.778Z', created_by: { email: 'elastic@elastic.co', full_name: 'Elastic', username: 'elastic' }, pushed_at: null, @@ -138,6 +140,7 @@ describe('Utils', () => { it('transform correctly without optional fields', () => { const comment = { comment: 'A comment', + type: 'user' as const, createdDate: '2020-04-09T09:43:51.778Z', }; @@ -145,6 +148,7 @@ describe('Utils', () => { expect(res).toEqual({ comment: 'A comment', + type: 'user', created_at: '2020-04-09T09:43:51.778Z', created_by: { email: undefined, full_name: undefined, username: undefined }, pushed_at: null, @@ -157,6 +161,7 @@ describe('Utils', () => { it('transform correctly with optional fields as null', () => { const comment = { comment: 'A comment', + type: 'user' as const, createdDate: '2020-04-09T09:43:51.778Z', email: null, full_name: null, @@ -167,6 +172,7 @@ describe('Utils', () => { expect(res).toEqual({ comment: 'A comment', + type: 'user', created_at: '2020-04-09T09:43:51.778Z', created_by: { email: null, full_name: null, username: null }, pushed_at: null, diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts index 3f82dac96a70e..a5ee0421affd0 100644 --- a/x-pack/plugins/case/server/routes/api/utils.ts +++ b/x-pack/plugins/case/server/routes/api/utils.ts @@ -57,6 +57,7 @@ export const transformNewCase = ({ interface NewCommentArgs { comment: string; + type: 'user' | 'alert'; createdDate: string; email?: string | null; full_name?: string | null; @@ -64,6 +65,7 @@ interface NewCommentArgs { } export const transformNewComment = ({ comment, + type, createdDate, email, // eslint-disable-next-line @typescript-eslint/naming-convention @@ -71,6 +73,7 @@ export const transformNewComment = ({ username, }: NewCommentArgs): CommentAttributes => ({ comment, + type, created_at: createdDate, created_by: { email, full_name, username }, pushed_at: null, diff --git a/x-pack/plugins/case/server/saved_object_types/comments.ts b/x-pack/plugins/case/server/saved_object_types/comments.ts index cb6f6ca84b1fd..87478eb23641f 100644 --- a/x-pack/plugins/case/server/saved_object_types/comments.ts +++ b/x-pack/plugins/case/server/saved_object_types/comments.ts @@ -5,6 +5,7 @@ */ import { SavedObjectsType } from 'src/core/server'; +import { commentsMigrations } from './migrations'; export const CASE_COMMENT_SAVED_OBJECT = 'cases-comments'; @@ -70,4 +71,5 @@ export const caseCommentSavedObjectType: SavedObjectsType = { }, }, }, + migrations: commentsMigrations, }; diff --git a/x-pack/plugins/case/server/saved_object_types/migrations.ts b/x-pack/plugins/case/server/saved_object_types/migrations.ts index c3dd88799b5fb..2bd8adfca6653 100644 --- a/x-pack/plugins/case/server/saved_object_types/migrations.ts +++ b/x-pack/plugins/case/server/saved_object_types/migrations.ts @@ -126,3 +126,27 @@ export const userActionsMigrations = { }; }, }; + +interface UnsanitizedComment { + comment: string; +} + +interface SanitizedComment { + comment: string; + type: 'user' | 'alert'; +} + +export const commentsMigrations = { + '7.11.0': ( + doc: SavedObjectUnsanitizedDoc + ): SavedObjectSanitizedDoc => { + return { + ...doc, + attributes: { + ...doc.attributes, + type: 'user', + }, + references: doc.references || [], + }; + }, +}; diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx index a85d7a310bc06..c270723bb18c6 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx @@ -12,6 +12,7 @@ import { TestProviders } from '../../../common/mock'; import { getFormMock } from '../__mock__/form'; import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; +import { CommentRequest } from '../../../../../case/common/api'; import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'; import { usePostComment } from '../../containers/use_post_comment'; import { useForm } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; @@ -66,8 +67,9 @@ const defaultPostCommment = { postComment, }; -const sampleData = { +const sampleData: CommentRequest = { comment: 'what a cool comment', + type: 'user', }; describe('AddComment ', () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx index 5b77c4d99a951..b6cc9e59d7815 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx @@ -27,6 +27,7 @@ const MySpinner = styled(EuiLoadingSpinner)` const initialCommentValue: CommentRequest = { comment: '', + type: 'user', }; export interface AddCommentRefObject { @@ -81,7 +82,7 @@ export const AddComment = React.memo( if (onCommentSaving != null) { onCommentSaving(); } - postComment(data, onCommentPosted); + postComment({ ...data, type: 'user' }, onCommentPosted); reset(); } }, [onCommentPosted, onCommentSaving, postComment, reset, submit]); diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_avatar.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_avatar.test.tsx index df5c51394b88a..fbebea6f1148f 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_avatar.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_avatar.test.tsx @@ -22,26 +22,18 @@ describe('UserActionAvatar ', () => { it('it renders', async () => { expect(wrapper.find(`[data-test-subj="user-action-avatar"]`).first().exists()).toBeTruthy(); - expect( - wrapper.find(`[data-test-subj="user-action-avatar-loading-spinner"]`).first().exists() - ).toBeFalsy(); expect(wrapper.find(`[data-test-subj="user-action-avatar"]`).first().text()).toBe('E'); }); it('it shows the username if the fullName is undefined', async () => { wrapper = mount(); expect(wrapper.find(`[data-test-subj="user-action-avatar"]`).first().exists()).toBeTruthy(); - expect( - wrapper.find(`[data-test-subj="user-action-avatar-loading-spinner"]`).first().exists() - ).toBeFalsy(); expect(wrapper.find(`[data-test-subj="user-action-avatar"]`).first().text()).toBe('e'); }); - it('shows the loading spinner when the username AND the fullName are undefined', async () => { + it('shows unknown when the username AND the fullName are undefined', async () => { wrapper = mount(); - expect(wrapper.find(`[data-test-subj="user-action-avatar"]`).first().exists()).toBeFalsy(); - expect( - wrapper.find(`[data-test-subj="user-action-avatar-loading-spinner"]`).first().exists() - ).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="user-action-avatar"]`).first().exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="user-action-avatar"]`).first().text()).toBe('U'); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx index 373202968f79b..82ac9936acb7c 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx @@ -404,6 +404,7 @@ describe('Case Configuration API', () => { }); const data = { comment: 'comment', + type: 'user' as const, }; test('check url, method, signal', async () => { diff --git a/x-pack/plugins/security_solution/public/cases/containers/mock.ts b/x-pack/plugins/security_solution/public/cases/containers/mock.ts index 218ed77399df0..d3b13f8b89961 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/mock.ts @@ -42,6 +42,7 @@ export const tags: string[] = ['coke', 'pepsi']; export const basicComment: Comment = { comment: 'Solve this fast!', + type: 'user' as const, id: basicCommentId, createdAt: basicCreatedAt, createdBy: elasticUser, diff --git a/x-pack/plugins/security_solution/public/cases/containers/types.ts b/x-pack/plugins/security_solution/public/cases/containers/types.ts index df3e75449b627..460dfa7088d0c 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/types.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/types.ts @@ -13,6 +13,7 @@ export interface Comment { createdAt: string; createdBy: ElasticUser; comment: string; + type: 'user' | 'alert'; pushedAt: string | null; pushedBy: string | null; updatedAt: string | null; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx index d7d9cf9c557c9..b41912ac9d0b6 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx @@ -15,6 +15,7 @@ describe('usePostComment', () => { const abortCtrl = new AbortController(); const samplePost = { comment: 'a comment', + type: 'user' as const, }; const updateCaseCallback = jest.fn(); beforeEach(() => { diff --git a/x-pack/test/case_api_integration/basic/tests/connectors/case.ts b/x-pack/test/case_api_integration/basic/tests/connectors/case.ts index 50fc1473e83d7..1da5157fb93b5 100644 --- a/x-pack/test/case_api_integration/basic/tests/connectors/case.ts +++ b/x-pack/test/case_api_integration/basic/tests/connectors/case.ts @@ -348,7 +348,7 @@ export default ({ getService }: FtrProviderContext): void => { const params = { subAction: 'update', subActionParams: { - comment: { comment: 'a comment' }, + comment: { comment: 'a comment', type: 'user' }, }, }; @@ -401,6 +401,41 @@ export default ({ getService }: FtrProviderContext): void => { }); }); + it('should respond with a 400 Bad Request when adding a comment to a case without comment type', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'update', + subActionParams: { + caseId: '123', + comment: { comment: 'a comment' }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/action/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subActionParams.id]: expected value of type [string] but got [undefined]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + it('should add a comment', async () => { const { body: createdAction } = await supertest .post('/api/actions/action') @@ -424,7 +459,7 @@ export default ({ getService }: FtrProviderContext): void => { subAction: 'addComment', subActionParams: { caseId: caseRes.body.id, - comment: { comment: 'a comment' }, + comment: { comment: 'a comment', type: 'user' }, }, }; From 484593c3a06945d4e45f601ef7aab23442fa95a4 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 30 Oct 2020 12:41:32 +0200 Subject: [PATCH 09/23] Pass connector to case client --- .../case/server/connectors/case/index.test.ts | 514 +++++++++++++++++- .../case/server/connectors/case/index.ts | 10 +- .../case/server/connectors/case/schema.ts | 56 ++ .../case/server/connectors/case/types.ts | 4 + .../case/server/connectors/case/validators.ts | 13 + .../plugins/case/server/connectors/index.ts | 3 +- .../basic/tests/connectors/case.ts | 270 +++++++++ 7 files changed, 858 insertions(+), 12 deletions(-) create mode 100644 x-pack/plugins/case/server/connectors/case/validators.ts diff --git a/x-pack/plugins/case/server/connectors/case/index.test.ts b/x-pack/plugins/case/server/connectors/case/index.test.ts index e741f33e5df53..4c1e0a01ef8da 100644 --- a/x-pack/plugins/case/server/connectors/case/index.test.ts +++ b/x-pack/plugins/case/server/connectors/case/index.test.ts @@ -51,6 +51,16 @@ describe('case connector', () => { title: 'Case from case connector!!', tags: ['case', 'connector'], description: 'Yo fields!!', + connector: { + id: 'jira', + name: 'Jira', + type: '.jira', + fields: { + issueType: '10006', + priority: 'High', + parent: null, + }, + }, }, }; @@ -66,6 +76,231 @@ describe('case connector', () => { validateParams(caseActionType, params); }).toThrow(); }); + + describe('connector', () => { + it('succeeds when jira fields are valid', () => { + const params: Record = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + tags: ['case', 'connector'], + description: 'Yo fields!!', + connector: { + id: 'jira', + name: 'Jira', + type: '.jira', + fields: { + issueType: '10006', + priority: 'High', + parent: null, + }, + }, + }, + }; + + expect(validateParams(caseActionType, params)).toEqual(params); + }); + + it('succeeds when resilient fields are valid', () => { + const params: Record = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + tags: ['case', 'connector'], + description: 'Yo fields!!', + connector: { + id: 'resilient', + name: 'Resilient', + type: '.resilient', + fields: { + incidentTypes: ['13'], + severityCode: '3', + }, + }, + }, + }; + + expect(validateParams(caseActionType, params)).toEqual(params); + }); + + it('succeeds when servicenow fields are valid', () => { + const params: Record = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + tags: ['case', 'connector'], + description: 'Yo fields!!', + connector: { + id: 'servicenow', + name: 'Servicenow', + type: '.servicenow', + fields: { + impact: 'Medium', + severity: 'Medium', + urgency: 'Medium', + }, + }, + }, + }; + + expect(validateParams(caseActionType, params)).toEqual(params); + }); + + it('set fields to null if they are missing', () => { + const params: Record = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + tags: ['case', 'connector'], + description: 'Yo fields!!', + connector: { + id: 'servicenow', + name: 'Servicenow', + type: '.servicenow', + fields: {}, + }, + }, + }; + + expect(validateParams(caseActionType, params)).toEqual({ + ...params, + subActionParams: { + title: 'Case from case connector!!', + tags: ['case', 'connector'], + description: 'Yo fields!!', + connector: { + id: 'servicenow', + name: 'Servicenow', + type: '.servicenow', + fields: { impact: null, severity: null, urgency: null }, + }, + }, + }); + }); + + it('succeeds when none fields are valid', () => { + const params: Record = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + tags: ['case', 'connector'], + description: 'Yo fields!!', + connector: { + id: 'none', + name: 'None', + type: '.none', + fields: null, + }, + }, + }; + + expect(validateParams(caseActionType, params)).toEqual(params); + }); + + it('fails when issueType is not provided', () => { + const params: Record = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + tags: ['case', 'connector'], + description: 'Yo fields!!', + connector: { + id: 'jira', + name: 'Jira', + type: '.jira', + fields: { + priority: 'High', + parent: null, + }, + }, + }, + }; + + expect(() => { + validateParams(caseActionType, params); + }).toThrow( + '[0.subActionParams.connector.fields.issueType]: expected value of type [string] but got [undefined]' + ); + }); + + it('fails with excess fields', () => { + const params: Record = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + tags: ['case', 'connector'], + description: 'Yo fields!!', + connector: { + id: 'servicenow', + name: 'Servicenow', + type: '.servicenow', + fields: { + impact: 'Medium', + severity: 'Medium', + urgency: 'Medium', + excess: null, + }, + }, + }, + }; + + expect(() => { + validateParams(caseActionType, params); + }).toThrow( + '[0.subActionParams.connector.fields.excess]: definition for this key is missing' + ); + }); + + it('fails with valid fields but wrong type', () => { + const params: Record = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + tags: ['case', 'connector'], + description: 'Yo fields!!', + connector: { + id: 'resilient', + name: 'Resilient', + type: '.resilient', + fields: { + issueType: '10006', + priority: 'High', + parent: null, + }, + }, + }, + }; + + expect(() => { + validateParams(caseActionType, params); + }).toThrow( + '[0.subActionParams.connector.fields.issueType]: definition for this key is missing' + ); + }); + + it('fails when fields are not null and the type is none', () => { + const params: Record = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + tags: ['case', 'connector'], + description: 'Yo fields!!', + connector: { + id: 'none', + name: 'None', + type: '.none', + fields: {}, + }, + }, + }; + + expect(() => { + validateParams(caseActionType, params); + }).toThrow( + '[0.subActionParams.connector]: Fields must be set to null for connectors of type .none' + ); + }); + }); }); describe('update', () => { @@ -86,11 +321,267 @@ describe('case connector', () => { tags: null, title: null, status: null, + connector: null, ...(params.subActionParams as Record), }, }); }); + describe('connector', () => { + it('succeeds when jira fields are valid', () => { + const params: Record = { + subAction: 'update', + subActionParams: { + id: 'case-id', + version: '123', + connector: { + id: 'jira', + name: 'Jira', + type: '.jira', + fields: { + issueType: '10006', + priority: 'High', + parent: null, + }, + }, + }, + }; + + expect(validateParams(caseActionType, params)).toEqual({ + ...params, + subActionParams: { + description: null, + tags: null, + title: null, + status: null, + ...(params.subActionParams as Record), + }, + }); + }); + + it('succeeds when resilient fields are valid', () => { + const params: Record = { + subAction: 'update', + subActionParams: { + id: 'case-id', + version: '123', + connector: { + id: 'resilient', + name: 'Resilient', + type: '.resilient', + fields: { + incidentTypes: ['13'], + severityCode: '3', + }, + }, + }, + }; + + expect(validateParams(caseActionType, params)).toEqual({ + ...params, + subActionParams: { + description: null, + tags: null, + title: null, + status: null, + ...(params.subActionParams as Record), + }, + }); + }); + + it('succeeds when servicenow fields are valid', () => { + const params: Record = { + subAction: 'update', + subActionParams: { + id: 'case-id', + version: '123', + connector: { + id: 'servicenow', + name: 'Servicenow', + type: '.servicenow', + fields: { + impact: 'Medium', + severity: 'Medium', + urgency: 'Medium', + }, + }, + }, + }; + + expect(validateParams(caseActionType, params)).toEqual({ + ...params, + subActionParams: { + description: null, + tags: null, + title: null, + status: null, + ...(params.subActionParams as Record), + }, + }); + }); + + it('set fields to null if they are missing', () => { + const params: Record = { + subAction: 'update', + subActionParams: { + id: 'case-id', + version: '123', + connector: { + id: 'servicenow', + name: 'Servicenow', + type: '.servicenow', + fields: {}, + }, + }, + }; + + expect(validateParams(caseActionType, params)).toEqual({ + ...params, + subActionParams: { + id: 'case-id', + version: '123', + description: null, + tags: null, + title: null, + status: null, + connector: { + id: 'servicenow', + name: 'Servicenow', + type: '.servicenow', + fields: { impact: null, severity: null, urgency: null }, + }, + }, + }); + }); + + it('succeeds when none fields are valid', () => { + const params: Record = { + subAction: 'update', + subActionParams: { + id: 'case-id', + version: '123', + connector: { + id: 'none', + name: 'None', + type: '.none', + fields: null, + }, + }, + }; + + expect(validateParams(caseActionType, params)).toEqual({ + ...params, + subActionParams: { + description: null, + tags: null, + title: null, + status: null, + ...(params.subActionParams as Record), + }, + }); + }); + + it('fails when issueType is not provided', () => { + const params: Record = { + subAction: 'update', + subActionParams: { + id: 'case-id', + version: '123', + connector: { + id: 'jira', + name: 'Jira', + type: '.jira', + fields: { + priority: 'High', + parent: null, + }, + }, + }, + }; + + expect(() => { + validateParams(caseActionType, params); + }).toThrow( + '[subActionParams.connector.0.fields.issueType]: expected value of type [string] but got [undefined]' + ); + }); + + it('fails with excess fields', () => { + const params: Record = { + subAction: 'update', + subActionParams: { + id: 'case-id', + version: '123', + connector: { + id: 'servicenow', + name: 'Servicenow', + type: '.servicenow', + fields: { + impact: 'Medium', + severity: 'Medium', + urgency: 'Medium', + excess: null, + }, + }, + }, + }; + + expect(() => { + validateParams(caseActionType, params); + }).toThrow( + '[subActionParams.connector.0.fields.excess]: definition for this key is missing' + ); + }); + + it('fails with valid fields but wrong type', () => { + const params: Record = { + subAction: 'update', + subActionParams: { + id: 'case-id', + version: '123', + connector: { + id: 'resilient', + name: 'Resilient', + type: '.resilient', + fields: { + issueType: '10006', + priority: 'High', + parent: null, + }, + }, + }, + }; + + expect(() => { + validateParams(caseActionType, params); + }).toThrow( + '[subActionParams.connector.0.fields.issueType]: definition for this key is missing' + ); + }); + + it('fails when fields are not null and the type is none', () => { + const params: Record = { + subAction: 'update', + subActionParams: { + id: 'case-id', + version: '123', + connector: { + id: 'none', + name: 'None', + type: '.none', + fields: {}, + }, + }, + }; + + expect(() => { + validateParams(caseActionType, params); + }).toThrow( + '[subActionParams.connector.0]: Fields must be set to null for connectors of type .none' + ); + }); + }); + it('fails when params is not valid', () => { const params: Record = { subAction: 'update', @@ -186,6 +677,16 @@ describe('case connector', () => { title: 'Case from case connector!!', tags: ['case', 'connector'], description: 'Yo fields!!', + connector: { + id: 'jira', + name: 'Jira', + type: '.jira', + fields: { + issueType: '10006', + priority: 'High', + parent: null, + }, + }, }, }; @@ -204,10 +705,14 @@ describe('case connector', () => { theCase: { ...params.subActionParams, connector: { - fields: null, - id: 'none', - name: 'none', - type: '.none', + id: 'jira', + name: 'Jira', + type: '.jira', + fields: { + issueType: '10006', + priority: 'High', + parent: null, + }, }, }, }); @@ -266,6 +771,7 @@ describe('case connector', () => { description: null, tags: null, status: null, + connector: null, }, }; diff --git a/x-pack/plugins/case/server/connectors/case/index.ts b/x-pack/plugins/case/server/connectors/case/index.ts index 12c4487f9c70d..6f0898e129147 100644 --- a/x-pack/plugins/case/server/connectors/case/index.ts +++ b/x-pack/plugins/case/server/connectors/case/index.ts @@ -8,7 +8,7 @@ import { curry } from 'lodash'; import { KibanaRequest } from 'kibana/server'; import { ActionTypeExecutorResult } from '../../../../actions/common'; -import { CasePatchRequest, ConnectorTypes } from '../../../common/api'; +import { CasePatchRequest, CasePostRequest } from '../../../common/api'; import { createCaseClient } from '../../client'; import { CaseExecutorParamsSchema, CaseConfigurationSchema } from './schema'; import { @@ -34,7 +34,7 @@ export function getActionType({ }: GetActionTypeParams): CaseActionType { return { id: '.case', - minimumLicenseRequired: 'platinum', + minimumLicenseRequired: 'gold', name: 'Case', validate: { config: CaseConfigurationSchema, @@ -71,12 +71,8 @@ async function executor( if (subAction === 'create') { const createParams = subActionParams as ExecutorSubActionCreateParams; - const theCase = { - ...createParams, - connector: { id: 'none', name: 'none', type: ConnectorTypes.none, fields: null }, - }; - data = await caseClient.create({ theCase }); + data = await caseClient.create({ theCase: createParams as CasePostRequest }); } if (subAction === 'update') { diff --git a/x-pack/plugins/case/server/connectors/case/schema.ts b/x-pack/plugins/case/server/connectors/case/schema.ts index b49bb4a4a0c10..aa503e96be30d 100644 --- a/x-pack/plugins/case/server/connectors/case/schema.ts +++ b/x-pack/plugins/case/server/connectors/case/schema.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { schema } from '@kbn/config-schema'; +import { validateConnector } from './validators'; // Reserved for future implementation export const CaseConfigurationSchema = schema.object({}); @@ -13,10 +14,64 @@ const CommentProps = { type: schema.oneOf([schema.literal('alert'), schema.literal('user')]), }; +const JiraFieldsSchema = schema.object({ + issueType: schema.string(), + priority: schema.nullable(schema.string()), + parent: schema.nullable(schema.string()), +}); + +const ResilientFieldsSchema = schema.object({ + incidentTypes: schema.nullable(schema.arrayOf(schema.string())), + severityCode: schema.nullable(schema.string()), +}); + +const ServiceNowFieldsSchema = schema.object({ + impact: schema.nullable(schema.string()), + severity: schema.nullable(schema.string()), + urgency: schema.nullable(schema.string()), +}); + +const NoneFieldsSchema = schema.nullable(schema.object({})); + +const ReducedConnectorFieldsSchema: { [x: string]: any } = { + '.jira': JiraFieldsSchema, + '.resilient': ResilientFieldsSchema, +}; + +export const ConnectorProps = { + id: schema.string(), + name: schema.string(), + type: schema.oneOf([ + schema.literal('.servicenow'), + schema.literal('.jira'), + schema.literal('.resilient'), + schema.literal('.none'), + ]), + // Chain of conditional schemes + fields: Object.keys(ReducedConnectorFieldsSchema).reduce( + (conditionalSchema, key) => + schema.conditional( + schema.siblingRef('type'), + key, + ReducedConnectorFieldsSchema[key], + conditionalSchema + ), + schema.conditional( + schema.siblingRef('type'), + '.servicenow', + ServiceNowFieldsSchema, + NoneFieldsSchema + ) + ), +}; + +export const ConnectorSchema = schema.object(ConnectorProps); + const CaseBasicProps = { description: schema.string(), title: schema.string(), tags: schema.arrayOf(schema.string()), + connector: schema.object(ConnectorProps, { validate: validateConnector }), }; const CaseUpdateRequestProps = { @@ -25,6 +80,7 @@ const CaseUpdateRequestProps = { description: schema.nullable(CaseBasicProps.description), title: schema.nullable(CaseBasicProps.title), tags: schema.nullable(CaseBasicProps.tags), + connector: schema.nullable(CaseBasicProps.connector), status: schema.nullable(schema.string()), }; diff --git a/x-pack/plugins/case/server/connectors/case/types.ts b/x-pack/plugins/case/server/connectors/case/types.ts index 394c2bfac2047..9b22caa599d23 100644 --- a/x-pack/plugins/case/server/connectors/case/types.ts +++ b/x-pack/plugins/case/server/connectors/case/types.ts @@ -12,10 +12,12 @@ import { ExecutorSubActionUpdateParamsSchema, CaseConfigurationSchema, ExecutorSubActionAddCommentParamsSchema, + ConnectorSchema, } from './schema'; import { CaseResponse, CasesResponse } from '../../../common/api'; export type CaseConfiguration = TypeOf; +export type Connector = TypeOf; export type ExecutorSubActionCreateParams = TypeOf; export type ExecutorSubActionUpdateParams = TypeOf; @@ -33,6 +35,8 @@ export type CaseActionType = ActionType< CaseExecutorResponse | {} >; +export type CaseActionTypeWithoutExecutor = ActionType; + export type CaseActionTypeExecutorOptions = ActionTypeExecutorOptions< CaseConfiguration, {}, diff --git a/x-pack/plugins/case/server/connectors/case/validators.ts b/x-pack/plugins/case/server/connectors/case/validators.ts new file mode 100644 index 0000000000000..f8330492d4366 --- /dev/null +++ b/x-pack/plugins/case/server/connectors/case/validators.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Connector } from './types'; + +export const validateConnector = (connector: Connector) => { + if (connector.type === '.none' && connector.fields !== null) { + return 'Fields must be set to null for connectors of type .none'; + } +}; diff --git a/x-pack/plugins/case/server/connectors/index.ts b/x-pack/plugins/case/server/connectors/index.ts index 425ea3f8b0037..04fe0eadd2107 100644 --- a/x-pack/plugins/case/server/connectors/index.ts +++ b/x-pack/plugins/case/server/connectors/index.ts @@ -19,6 +19,7 @@ import { } from '../services'; import { getActionType as getCaseConnector } from './case'; +import { CaseActionTypeWithoutExecutor } from './case/types'; export interface GetActionTypeParams { logger: Logger; @@ -50,6 +51,6 @@ export const registerConnectors = ({ caseService, caseConfigureService, userActionService, - }) + }) as CaseActionTypeWithoutExecutor ); }; diff --git a/x-pack/test/case_api_integration/basic/tests/connectors/case.ts b/x-pack/test/case_api_integration/basic/tests/connectors/case.ts index 1da5157fb93b5..7a351d09b5b9f 100644 --- a/x-pack/test/case_api_integration/basic/tests/connectors/case.ts +++ b/x-pack/test/case_api_integration/basic/tests/connectors/case.ts @@ -74,6 +74,16 @@ export default ({ getService }: FtrProviderContext): void => { subActionParams: { tags: ['case', 'connector'], description: 'case description', + connector: { + id: 'jira', + name: 'Jira', + type: '.jira', + fields: { + issueType: '10006', + priority: 'High', + parent: null, + }, + }, }, }; @@ -109,6 +119,16 @@ export default ({ getService }: FtrProviderContext): void => { subActionParams: { title: 'Case from case connector!!', tags: ['case', 'connector'], + connector: { + id: 'jira', + name: 'Jira', + type: '.jira', + fields: { + issueType: '10006', + priority: 'High', + parent: null, + }, + }, }, }; @@ -144,6 +164,16 @@ export default ({ getService }: FtrProviderContext): void => { subActionParams: { title: 'Case from case connector!!', description: 'case description', + connector: { + id: 'jira', + name: 'Jira', + type: '.jira', + fields: { + issueType: '10006', + priority: 'High', + parent: null, + }, + }, }, }; @@ -162,6 +192,175 @@ export default ({ getService }: FtrProviderContext): void => { }); }); + it('should respond with a 400 Bad Request when creating a case without connector', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + description: 'case description', + tags: ['case', 'connector'], + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/action/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subActionParams.connector.id]: expected value of type [string] but got [undefined]\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should respond with a 400 Bad Request when creating jira without issueType', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + description: 'case description', + tags: ['case', 'connector'], + connector: { + id: 'jira', + name: 'Jira', + type: '.jira', + fields: { + priority: 'High', + parent: null, + }, + }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/action/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subActionParams.connector.fields.issueType]: expected value of type [string] but got [undefined]\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should respond with a 400 Bad Request when creating a connector with wrong fields', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + description: 'case description', + tags: ['case', 'connector'], + connector: { + id: 'servicenow', + name: 'Servicenow', + type: '.servicenow', + fields: { + impact: 'Medium', + severity: 'Medium', + notExists: 'not-exists', + }, + }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/action/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subActionParams.connector.fields.notExists]: definition for this key is missing\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should respond with a 400 Bad Request when creating a none without fields as null', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + description: 'case description', + tags: ['case', 'connector'], + connector: { + id: 'none', + name: 'None', + type: '.none', + fields: {}, + }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/action/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subActionParams.connector]: Fields must be set to null for connectors of type .none\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + it('should create a case', async () => { const { body: createdAction } = await supertest .post('/api/actions/action') @@ -180,6 +379,16 @@ export default ({ getService }: FtrProviderContext): void => { title: 'Case from case connector!!', tags: ['case', 'connector'], description: 'case description', + connector: { + id: 'jira', + name: 'Jira', + type: '.jira', + fields: { + issueType: '10006', + priority: 'High', + parent: null, + }, + }, }, }; @@ -206,6 +415,67 @@ export default ({ getService }: FtrProviderContext): void => { }, }); }); + + it('should create a case with connector with field as null if not provided', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + tags: ['case', 'connector'], + description: 'case description', + connector: { + id: 'servicenow', + name: 'Servicenow', + type: '.servicenow', + fields: {}, + }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/action/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${caseConnector.body.data.id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const data = removeServerGeneratedPropertiesFromCase(body); + expect(data).to.eql({ + ...postCaseResp(caseConnector.body.data.id), + ...params.subActionParams, + connector: { + id: 'servicenow', + name: 'Servicenow', + type: '.servicenow', + fields: { + impact: null, + severity: null, + urgency: null, + }, + }, + created_by: { + email: null, + full_name: null, + username: null, + }, + }); + }); }); describe('update', () => { From bf2789f4357426e5e2cf494387d85c6193657f98 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 30 Oct 2020 17:12:32 +0200 Subject: [PATCH 10/23] Improve type after PR #82125 --- x-pack/plugins/case/server/connectors/case/types.ts | 2 -- x-pack/plugins/case/server/connectors/index.ts | 8 ++++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/case/server/connectors/case/types.ts b/x-pack/plugins/case/server/connectors/case/types.ts index 9b22caa599d23..b3a05163fa6f4 100644 --- a/x-pack/plugins/case/server/connectors/case/types.ts +++ b/x-pack/plugins/case/server/connectors/case/types.ts @@ -35,8 +35,6 @@ export type CaseActionType = ActionType< CaseExecutorResponse | {} >; -export type CaseActionTypeWithoutExecutor = ActionType; - export type CaseActionTypeExecutorOptions = ActionTypeExecutorOptions< CaseConfiguration, {}, diff --git a/x-pack/plugins/case/server/connectors/index.ts b/x-pack/plugins/case/server/connectors/index.ts index 04fe0eadd2107..6a97a9e6e8a8a 100644 --- a/x-pack/plugins/case/server/connectors/index.ts +++ b/x-pack/plugins/case/server/connectors/index.ts @@ -19,7 +19,6 @@ import { } from '../services'; import { getActionType as getCaseConnector } from './case'; -import { CaseActionTypeWithoutExecutor } from './case/types'; export interface GetActionTypeParams { logger: Logger; @@ -32,9 +31,10 @@ export interface RegisterConnectorsArgs extends GetActionTypeParams { actionsRegisterType< Config extends ActionTypeConfig = ActionTypeConfig, Secrets extends ActionTypeSecrets = ActionTypeSecrets, - Params extends ActionTypeParams = ActionTypeParams + Params extends ActionTypeParams = ActionTypeParams, + ExecutorResultData = void >( - actionType: ActionType + actionType: ActionType ): void; } @@ -51,6 +51,6 @@ export const registerConnectors = ({ caseService, caseConfigureService, userActionService, - }) as CaseActionTypeWithoutExecutor + }) ); }; From 7af22e410204951b36f64e004ace1a921b27bfa2 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 30 Oct 2020 18:44:19 +0200 Subject: [PATCH 11/23] Add comment migration test --- .../basic/tests/cases/comments/migrations.ts | 36 ++++++++++ .../functional/es_archives/cases/data.json | 72 +++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 x-pack/test/case_api_integration/basic/tests/cases/comments/migrations.ts diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/migrations.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/migrations.ts new file mode 100644 index 0000000000000..a96197cee5f3b --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/migrations.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { CASES_URL } from '../../../../../../plugins/case/common/constants'; + +// eslint-disable-next-line import/no-default-export +export default function createGetTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('migrations', () => { + before(async () => { + await esArchiver.load('cases'); + }); + + after(async () => { + await esArchiver.unload('cases'); + }); + + it('7.11.0 migrates cases comments', async () => { + const { body: comment } = await supertest + .get( + `${CASES_URL}/e1900ac0-017f-11eb-93f8-d161651bf509/comments/da677740-1ac7-11eb-b5a3-25ee88122510` + ) + .set('kbn-xsrf', 'true') + .send(); + + expect(comment.type).to.eql('user'); + }); + }); +} diff --git a/x-pack/test/functional/es_archives/cases/data.json b/x-pack/test/functional/es_archives/cases/data.json index 2ca805259e318..9af1ac47b61a7 100644 --- a/x-pack/test/functional/es_archives/cases/data.json +++ b/x-pack/test/functional/es_archives/cases/data.json @@ -137,3 +137,75 @@ "type": "_doc" } } + +{ + "type": "doc", + "value": { + "id": "cases-comments:da677740-1ac7-11eb-b5a3-25ee88122510", + "index": ".kibana_1", + "source": { + "cases-comments": { + "comment": "This is a cool comment", + "created_at": "2020-10-30T15:52:02.984Z", + "created_by": { + "email": null, + "full_name": null, + "username": "elastic" + }, + "pushed_at": null, + "pushed_by": null, + "updated_at": null, + "updated_by": null + }, + "references": [ + { + "id": "e1900ac0-017f-11eb-93f8-d161651bf509", + "name": "associated-cases", + "type": "cases" + } + ], + "type": "cases-comments", + "updated_at": "2020-10-30T15:52:02.996Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "cases-user-actions:db027ec0-1ac7-11eb-b5a3-25ee88122510", + "index": ".kibana_1", + "source": { + "cases-user-actions": { + "action": "create", + "action_at": "2020-10-30T15:52:02.984Z", + "action_by": { + "email": null, + "full_name": null, + "username": "elastic" + }, + "action_field": [ + "comment" + ], + "new_value": "This is a cool comment", + "old_value": null + }, + "references": [ + { + "id": "e1900ac0-017f-11eb-93f8-d161651bf509", + "name": "associated-cases", + "type": "cases" + }, + { + "id": "da677740-1ac7-11eb-b5a3-25ee88122510", + "name": "associated-cases-comments", + "type": "cases-comments" + } + ], + "type": "cases-user-actions", + "updated_at": "2020-10-30T15:52:04.012Z" + }, + "type": "_doc" + } +} From 6cc8b62f8f6a0405482a28c61595f9e0853bde80 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 30 Oct 2020 18:44:42 +0200 Subject: [PATCH 12/23] Fix integration tests --- .../tests/cases/comments/delete_comment.ts | 8 ++- .../tests/cases/comments/find_comments.ts | 25 +++++++--- .../basic/tests/cases/comments/get_comment.ts | 6 ++- .../tests/cases/comments/patch_comment.ts | 20 ++++++-- .../tests/cases/comments/post_comment.ts | 4 +- .../basic/tests/cases/delete_cases.ts | 12 +++-- .../basic/tests/cases/find_cases.ts | 41 +++++++++++++--- .../basic/tests/cases/patch_cases.ts | 4 ++ .../basic/tests/cases/push_case.ts | 4 +- .../basic/tests/cases/status/get_status.ts | 4 +- .../basic/tests/cases/tags/get_tags.ts | 3 +- .../user_actions/get_all_user_actions.ts | 49 ++++++++++++++----- .../case_api_integration/common/lib/mock.ts | 3 +- 13 files changed, 138 insertions(+), 45 deletions(-) diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts index afae04ae9cf5b..5fb6f21c51c95 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts @@ -33,11 +33,13 @@ export default ({ getService }: FtrProviderContext): void => { const { body: patchedCase } = await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq); + .send(postCommentReq) + .expect(200); const { body: comment } = await supertest .delete(`${CASES_URL}/${postedCase.id}/comments/${patchedCase.comments[0].id}`) .set('kbn-xsrf', 'true') + .expect(204) .send(); expect(comment).to.eql({}); @@ -53,13 +55,15 @@ export default ({ getService }: FtrProviderContext): void => { const { body: patchedCase } = await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq); + .send(postCommentReq) + .expect(200); const { body } = await supertest .delete(`${CASES_URL}/fake-id/comments/${patchedCase.comments[0].id}`) .set('kbn-xsrf', 'true') .send() .expect(404); + expect(body.message).to.eql( `This comment ${patchedCase.comments[0].id} does not exist in fake-id).` ); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts index e5c44de90b5a1..c67eda1d3a16b 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts @@ -29,21 +29,25 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .send(postCaseReq) .expect(200); + // post 2 comments await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq); + .send(postCommentReq) + .expect(200); const { body: patchedCase } = await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq); + .send(postCommentReq) + .expect(200); const { body: caseComments } = await supertest .get(`${CASES_URL}/${postedCase.id}/comments/_find`) .set('kbn-xsrf', 'true') - .send(); + .send() + .expect(200); expect(caseComments.comments).to.eql(patchedCase.comments); }); @@ -54,21 +58,25 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .send(postCaseReq) .expect(200); + // post 2 comments await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq); + .send(postCommentReq) + .expect(200); const { body: patchedCase } = await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send({ comment: 'unique' }); + .send({ comment: 'unique', type: 'user' }) + .expect(200); const { body: caseComments } = await supertest .get(`${CASES_URL}/${postedCase.id}/comments/_find?search=unique`) .set('kbn-xsrf', 'true') - .send(); + .send() + .expect(200); expect(caseComments.comments).to.eql([patchedCase.comments[1]]); }); @@ -79,10 +87,13 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .send(postCaseReq) .expect(200); + await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq); + .send(postCommentReq) + .expect(200); + await supertest .get(`${CASES_URL}/${postedCase.id}/comments/_find?perPage=true`) .set('kbn-xsrf', 'true') diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts index 53da0ef1d2b16..9c3a85e99c29d 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts @@ -27,12 +27,14 @@ export default ({ getService }: FtrProviderContext): void => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') - .send(postCaseReq); + .send(postCaseReq) + .expect(200); const { body: patchedCase } = await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq); + .send(postCommentReq) + .expect(200); const { body: comment } = await supertest .get(`${CASES_URL}/${postedCase.id}/comments/${patchedCase.comments[0].id}`) diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts index 73aeeb0fb989a..3176841b009d4 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts @@ -33,7 +33,9 @@ export default ({ getService }: FtrProviderContext): void => { const { body: patchedCase } = await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq); + .send(postCommentReq) + .expect(200); + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; const { body } = await supertest .patch(`${CASES_URL}/${postedCase.id}/comments`) @@ -42,7 +44,9 @@ export default ({ getService }: FtrProviderContext): void => { id: patchedCase.comments[0].id, version: patchedCase.comments[0].version, comment: newComment, - }); + }) + .expect(200); + expect(body.comments[0].comment).to.eql(newComment); expect(body.updated_by).to.eql(defaultUser); }); @@ -51,7 +55,9 @@ export default ({ getService }: FtrProviderContext): void => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') - .send(postCaseReq); + .send(postCaseReq) + .expect(200); + await supertest .patch(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') @@ -85,7 +91,9 @@ export default ({ getService }: FtrProviderContext): void => { const { body: patchedCase } = await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq); + .send(postCommentReq) + .expect(200); + await supertest .patch(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') @@ -107,7 +115,9 @@ export default ({ getService }: FtrProviderContext): void => { const { body: patchedCase } = await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq); + .send(postCommentReq) + .expect(200); + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; await supertest .patch(`${CASES_URL}/${postedCase.id}/comments`) diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts index 6e8353f8ea86a..0c7ab52abf8c8 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts @@ -33,7 +33,9 @@ export default ({ getService }: FtrProviderContext): void => { const { body: patchedCase } = await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq); + .send(postCommentReq) + .expect(200); + expect(patchedCase.comments[0].comment).to.eql(postCommentReq.comment); expect(patchedCase.updated_by).to.eql(defaultUser); }); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts index aa2465e44c5c1..73d17b985216a 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts @@ -27,7 +27,8 @@ export default ({ getService }: FtrProviderContext): void => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') - .send(postCaseReq); + .send(postCaseReq) + .expect(200); const { body } = await supertest .delete(`${CASES_URL}?ids=["${postedCase.id}"]`) @@ -42,29 +43,34 @@ export default ({ getService }: FtrProviderContext): void => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') - .send(postCaseReq); + .send(postCaseReq) + .expect(200); const { body: patchedCase } = await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq); + .send(postCommentReq) + .expect(200); await supertest .get(`${CASES_URL}/${postedCase.id}/comments/${patchedCase.comments[0].id}`) .set('kbn-xsrf', 'true') .send() .expect(200); + await supertest .delete(`${CASES_URL}?ids=["${postedCase.id}"]`) .set('kbn-xsrf', 'true') .send() .expect(204); + await supertest .get(`${CASES_URL}/${postedCase.id}/comments/${patchedCase.comments[0].id}`) .set('kbn-xsrf', 'true') .send() .expect(404); }); + it('unhappy path - 404s when case is not there', async () => { await supertest .delete(`${CASES_URL}?ids=["fake-id"]`) diff --git a/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts index 39762866ac506..17814868fecc0 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts @@ -33,9 +33,24 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should return cases', async () => { - const { body: a } = await supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(postCaseReq); - const { body: b } = await supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(postCaseReq); - const { body: c } = await supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(postCaseReq); + const { body: a } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const { body: b } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const { body: c } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + const { body } = await supertest .get(`${CASES_URL}/_find?sortOrder=asc`) .set('kbn-xsrf', 'true') @@ -55,7 +70,9 @@ export default ({ getService }: FtrProviderContext): void => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') - .send({ ...postCaseReq, tags: ['unique'] }); + .send({ ...postCaseReq, tags: ['unique'] }) + .expect(200); + const { body } = await supertest .get(`${CASES_URL}/_find?sortOrder=asc&tags=unique`) .set('kbn-xsrf', 'true') @@ -74,17 +91,22 @@ export default ({ getService }: FtrProviderContext): void => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') - .send(postCaseReq); + .send(postCaseReq) + .expect(200); // post 2 comments await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq); + .send(postCommentReq) + .expect(200); + const { body: patchedCase } = await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq); + .send(postCommentReq) + .expect(200); + const { body } = await supertest .get(`${CASES_URL}/_find?sortOrder=asc`) .set('kbn-xsrf', 'true') @@ -110,7 +132,9 @@ export default ({ getService }: FtrProviderContext): void => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') - .send(postCaseReq); + .send(postCaseReq) + .expect(200); + await supertest .patch(CASES_URL) .set('kbn-xsrf', 'true') @@ -124,6 +148,7 @@ export default ({ getService }: FtrProviderContext): void => { ], }) .expect(200); + const { body } = await supertest .get(`${CASES_URL}/_find?sortOrder=asc`) .set('kbn-xsrf', 'true') diff --git a/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts index 861a1ce78cf7c..08e80bef34555 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts @@ -118,6 +118,7 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .send(postCaseReq) .expect(200); + await supertest .patch(CASES_URL) .set('kbn-xsrf', 'true') @@ -139,6 +140,7 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .send(postCaseReq) .expect(200); + await supertest .patch(CASES_URL) .set('kbn-xsrf', 'true') @@ -160,6 +162,7 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .send(postCaseReq) .expect(200); + await supertest .patch(CASES_URL) .set('kbn-xsrf', 'true') @@ -181,6 +184,7 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .send(postCaseReq) .expect(200); + await supertest .patch(CASES_URL) .set('kbn-xsrf', 'true') diff --git a/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts b/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts index 0d3d3df5bbd17..80cf2c8199807 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts @@ -130,7 +130,8 @@ export default ({ getService }: FtrProviderContext): void => { await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq); + .send(postCommentReq) + .expect(200); const { body } = await supertest .post(`${CASES_URL}/${postedCase.id}/_push`) @@ -143,6 +144,7 @@ export default ({ getService }: FtrProviderContext): void => { external_url: 'external_url', }) .expect(200); + expect(body.comments[0].pushed_by).to.eql(defaultUser); }); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/status/get_status.ts b/x-pack/test/case_api_integration/basic/tests/cases/status/get_status.ts index 737f90abf512b..d3cd69384b93d 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/status/get_status.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/status/get_status.ts @@ -26,7 +26,9 @@ export default ({ getService }: FtrProviderContext): void => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') - .send(postCaseReq); + .send(postCaseReq) + .expect(200); + await supertest .patch(CASES_URL) .set('kbn-xsrf', 'true') diff --git a/x-pack/test/case_api_integration/basic/tests/cases/tags/get_tags.ts b/x-pack/test/case_api_integration/basic/tests/cases/tags/get_tags.ts index 515cb72424e2a..71e370809c3c7 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/tags/get_tags.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/tags/get_tags.ts @@ -26,7 +26,8 @@ export default ({ getService }: FtrProviderContext): void => { await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') - .send({ ...postCaseReq, tags: ['unique'] }); + .send({ ...postCaseReq, tags: ['unique'] }) + .expect(200); const { body } = await supertest .get(CASE_TAGS_URL) diff --git a/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts index e53013348c66b..92ef544ee9b37 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts @@ -39,13 +39,15 @@ export default ({ getService }: FtrProviderContext): void => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') - .send(postCaseReq); + .send(postCaseReq) + .expect(200); const { body } = await supertest .get(`${CASES_URL}/${postedCase.id}/user_actions`) .set('kbn-xsrf', 'true') .send() .expect(200); + expect(body.length).to.eql(1); expect(body[0].action_field).to.eql(['description', 'status', 'tags', 'title', 'connector']); @@ -58,7 +60,9 @@ export default ({ getService }: FtrProviderContext): void => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') - .send(postCaseReq); + .send(postCaseReq) + .expect(200); + await supertest .patch(CASES_URL) .set('kbn-xsrf', 'true') @@ -78,6 +82,7 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .send() .expect(200); + expect(body.length).to.eql(2); expect(body[1].action_field).to.eql(['status']); expect(body[1].action).to.eql('update'); @@ -89,7 +94,8 @@ export default ({ getService }: FtrProviderContext): void => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') - .send(postCaseReq); + .send(postCaseReq) + .expect(200); const newConnector = { id: '123', @@ -117,6 +123,7 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .send() .expect(200); + expect(body.length).to.eql(2); expect(body[1].action_field).to.eql(['connector']); expect(body[1].action).to.eql('update'); @@ -130,7 +137,9 @@ export default ({ getService }: FtrProviderContext): void => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') - .send(postCaseReq); + .send(postCaseReq) + .expect(200); + await supertest .patch(CASES_URL) .set('kbn-xsrf', 'true') @@ -150,6 +159,7 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .send() .expect(200); + expect(body.length).to.eql(3); expect(body[1].action_field).to.eql(['tags']); expect(body[1].action).to.eql('add'); @@ -165,7 +175,9 @@ export default ({ getService }: FtrProviderContext): void => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') - .send(postCaseReq); + .send(postCaseReq) + .expect(200); + const newTitle = 'Such a great title'; await supertest .patch(CASES_URL) @@ -186,6 +198,7 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .send() .expect(200); + expect(body.length).to.eql(2); expect(body[1].action_field).to.eql(['title']); expect(body[1].action).to.eql('update'); @@ -197,7 +210,9 @@ export default ({ getService }: FtrProviderContext): void => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') - .send(postCaseReq); + .send(postCaseReq) + .expect(200); + const newDesc = 'Such a great description'; await supertest .patch(CASES_URL) @@ -218,6 +233,7 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .send() .expect(200); + expect(body.length).to.eql(2); expect(body[1].action_field).to.eql(['description']); expect(body[1].action).to.eql('update'); @@ -229,19 +245,22 @@ export default ({ getService }: FtrProviderContext): void => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') - .send(postCaseReq); + .send(postCaseReq) + .expect(200); + await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq); + .send(postCommentReq) + .expect(200); const { body } = await supertest .get(`${CASES_URL}/${postedCase.id}/user_actions`) .set('kbn-xsrf', 'true') .send() .expect(200); - expect(body.length).to.eql(2); + expect(body.length).to.eql(2); expect(body[1].action_field).to.eql(['comment']); expect(body[1].action).to.eql('create'); expect(body[1].old_value).to.eql(null); @@ -252,11 +271,15 @@ export default ({ getService }: FtrProviderContext): void => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') - .send(postCaseReq); + .send(postCaseReq) + .expect(200); + const { body: patchedCase } = await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send(postCommentReq); + .send(postCommentReq) + .expect(200); + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; await supertest.patch(`${CASES_URL}/${postedCase.id}/comments`).set('kbn-xsrf', 'true').send({ id: patchedCase.comments[0].id, @@ -269,8 +292,8 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .send() .expect(200); - expect(body.length).to.eql(3); + expect(body.length).to.eql(3); expect(body[2].action_field).to.eql(['comment']); expect(body[2].action).to.eql('update'); expect(body[2].old_value).to.eql(postCommentReq.comment); @@ -329,8 +352,8 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .send() .expect(200); - expect(body.length).to.eql(2); + expect(body.length).to.eql(2); expect(body[1].action_field).to.eql(['pushed']); expect(body[1].action).to.eql('push-to-service'); expect(body[1].old_value).to.eql(null); diff --git a/x-pack/test/case_api_integration/common/lib/mock.ts b/x-pack/test/case_api_integration/common/lib/mock.ts index 72e1639143eae..d2262c684dc6d 100644 --- a/x-pack/test/case_api_integration/common/lib/mock.ts +++ b/x-pack/test/case_api_integration/common/lib/mock.ts @@ -24,8 +24,9 @@ export const postCaseReq: CasePostRequest = { }, }; -export const postCommentReq: { comment: string } = { +export const postCommentReq: { comment: string; type: string } = { comment: 'This is a cool comment', + type: 'user', }; export const postCaseResp = ( From 1f814e20fe4faac8ee85d1c8553105ade8c4dafe Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 2 Nov 2020 12:53:53 +0200 Subject: [PATCH 13/23] Fix reporter on table --- .../public/cases/components/all_cases/columns.tsx | 4 ++-- .../public/cases/components/case_view/translations.ts | 4 ---- x-pack/plugins/security_solution/public/cases/translations.ts | 4 ++++ 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.tsx index 5c6c72477bf1f..42b97d5f6130f 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.tsx @@ -82,11 +82,11 @@ export const getCasesColumns = ( <> - {createdBy.fullName ? createdBy.fullName : createdBy.username ?? ''} + {createdBy.fullName ? createdBy.fullName : createdBy.username ?? i18n.UNKNOWN} ); diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/translations.ts b/x-pack/plugins/security_solution/public/cases/components/case_view/translations.ts index 04bb8801c9f00..ac518a9cc2fb0 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/translations.ts @@ -152,10 +152,6 @@ export const EMAIL_BODY = (caseUrl: string) => defaultMessage: 'Case reference: {caseUrl}', }); -export const UNKNOWN = i18n.translate('xpack.securitySolution.case.caseView.unknown', { - defaultMessage: 'Unknown', -}); - export const CHANGED_CONNECTOR_FIELD = i18n.translate( 'xpack.securitySolution.case.caseView.fieldChanged', { diff --git a/x-pack/plugins/security_solution/public/cases/translations.ts b/x-pack/plugins/security_solution/public/cases/translations.ts index a0b5f71db7df0..1d60310731d5e 100644 --- a/x-pack/plugins/security_solution/public/cases/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/translations.ts @@ -234,3 +234,7 @@ export const EDIT_CONNECTOR = i18n.translate('xpack.securitySolution.case.caseVi export const NO_CONNECTOR = i18n.translate('xpack.securitySolution.case.common.noConnector', { defaultMessage: 'No connector selected', }); + +export const UNKNOWN = i18n.translate('xpack.securitySolution.case.caseView.unknown', { + defaultMessage: 'Unknown', +}); From 503f6d94cdaf9df8610799cbbfc0b734dcef2886 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 2 Nov 2020 12:55:13 +0200 Subject: [PATCH 14/23] Create case connector ui --- .../common/lib/connectors/case/index.ts | 22 +++++++++++++++++++ .../lib/connectors/case/translations.ts | 21 ++++++++++++++++++ .../public/common/lib/connectors/index.ts | 7 ++++++ .../security_solution/public/plugin.tsx | 3 +++ 4 files changed, 53 insertions(+) create mode 100644 x-pack/plugins/security_solution/public/common/lib/connectors/case/index.ts create mode 100644 x-pack/plugins/security_solution/public/common/lib/connectors/case/translations.ts create mode 100644 x-pack/plugins/security_solution/public/common/lib/connectors/index.ts diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/case/index.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/case/index.ts new file mode 100644 index 0000000000000..271b1bfd2e3de --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/connectors/case/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ActionTypeModel } from '../../../../../../triggers_actions_ui/public/types'; +import * as i18n from './translations'; + +export function getActionType(): ActionTypeModel { + return { + id: '.case', + iconClass: 'securityAnalyticsApp', + selectMessage: i18n.CASE_CONNECTOR_DESC, + actionTypeTitle: i18n.CASE_CONNECTOR_TITLE, + validateConnector: () => ({ errors: {} }), + validateParams: () => ({ errors: {} }), + actionConnectorFields: null, + actionParamsFields: null, + }; +} diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/case/translations.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/case/translations.ts new file mode 100644 index 0000000000000..a39e04acc1bf3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/connectors/case/translations.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const CASE_CONNECTOR_DESC = i18n.translate( + 'xpack.securitySolution.case.components.case.selectMessageText', + { + defaultMessage: 'Create or update a case.', + } +); + +export const CASE_CONNECTOR_TITLE = i18n.translate( + 'xpack.securitySolution.case.components.case.actionTypeTitle', + { + defaultMessage: 'Cases', + } +); diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts new file mode 100644 index 0000000000000..58d7e89e080e7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { getActionType as getCaseConnectorUI } from './case'; diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index b81bea1d12b27..08c780d4a7203 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -62,6 +62,7 @@ import { IndexFieldsStrategyResponse, } from '../common/search_strategy/index_fields'; import { SecurityAppStore } from './common/store/store'; +import { getCaseConnectorUI } from './common/lib/connectors'; export class Plugin implements IPlugin { private kibanaVersion: string; @@ -312,6 +313,8 @@ export class Plugin implements IPlugin { /** From 703e5e4da598dc9854203e4f7f47033ee74da16e Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 2 Nov 2020 15:45:00 +0200 Subject: [PATCH 15/23] Add connector to README --- x-pack/plugins/actions/README.md | 120 ++++++++++++++++++++++++------- 1 file changed, 94 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 02e8e91c987d8..82c0870ae496e 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -1,4 +1,3 @@ -# Kibana Actions The Kibana actions plugin provides a framework to create executable actions. You can: @@ -14,31 +13,6 @@ The Kibana actions plugin provides a framework to create executable actions. You Table of Contents -- [Kibana Actions](#kibana-actions) - - [Terminology](#terminology) - - [Usage](#usage) - - [Kibana Actions Configuration](#kibana-actions-configuration) - - [Configuration Options](#configuration-options) - - [Adding Built-in Action Types to allowedHosts](#adding-built-in-action-types-to-allowedhosts) - - [Configuration Utilities](#configuration-utilities) - - [Action types](#action-types) - - [Methods](#methods) - - [Executor](#executor) - - [Example](#example) - - [RESTful API](#restful-api) - - [`POST /api/actions/action`: Create action](#post-apiactionsaction-create-action) - - [`DELETE /api/actions/action/{id}`: Delete action](#delete-apiactionsactionid-delete-action) - - [`GET /api/actions`: Get all actions](#get-apiactions-get-all-actions) - - [`GET /api/actions/action/{id}`: Get action](#get-apiactionsactionid-get-action) - - [`GET /api/actions/list_action_types`: List action types](#get-apiactionslist_action_types-list-action-types) - - [`PUT /api/actions/action/{id}`: Update action](#put-apiactionsactionid-update-action) - - [`POST /api/actions/action/{id}/_execute`: Execute action](#post-apiactionsactionid_execute-execute-action) - - [Firing actions](#firing-actions) - - [Accessing a scoped ActionsClient](#accessing-a-scoped-actionsclient) - - [actionsClient.enqueueExecution(options)](#actionsclientenqueueexecutionoptions) - - [Example](#example-1) - - [actionsClient.execute(options)](#actionsclientexecuteoptions) - - [Example](#example-2) - [Built-in Action Types](#built-in-action-types) - [Server log](#server-log) - [`config`](#config) @@ -81,6 +55,15 @@ Table of Contents - [`secrets`](#secrets-8) - [`params`](#params-8) - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-3) + - [Case](#case) + - [`config`](#config-9) + - [`secrets`](#secrets-9) + - [`params`](#params-9) + - [`subActionParams (create)`](#subactionparams-create) + - [`subActionParams (update)`](#subactionparams-update) + - [`subActionParams (addComment)`](#subactionparams-addcomment) + - [`connector`](#connector) + - [`fields`](#fields) - [Command Line Utility](#command-line-utility) - [Developing New Action Types](#developing-new-action-types) - [licensing](#licensing) @@ -670,6 +653,91 @@ ID: `.resilient` | incidentTypes | An array with the ids of IBM Resilient incident types. | number[] _(optional)_ | | severityCode | IBM Resilient id of the severity code. | number _(optional)_ | +## Case + +ID: `.case` + +The params properties are modelled after the arguments to the [Cases API](https://www.elastic.co/guide/en/security/master/cases-api-overview.html). + +### `config` + +This action has no `config` properties. + +### `secrets` + +This action type has no `secrets` properties. + +### `params` + +| Property | Description | Type | +| --------------- | ------------------------------------------------------------------------- | ------ | +| subAction | The sub action to perform. It can be `create`, `update`, and `addComment` | string | +| subActionParams | The parameters of the sub action | object | + +#### `subActionParams (create)` + +| Property | Description | Type | +| ----------- | --------------------------------------------------------------------- | ----------------------- | +| tile | The case’s title. | string | +| description | The case’s description. | string | +| tags | String array containing words and phrases that help categorize cases. | string[] | +| connector | Object containing the connector’s configuration. | [connector](#connector) | + +#### `subActionParams (update)` + +| Property | Description | Type | +| ----------- | ---------------------------------------------------------- | ----------------------- | +| id | The ID of the case being updated. | string | +| tile | The updated case title. | string | +| description | The updated case description. | string | +| tags | The updated case tags. | string | +| connector | Object containing the connector’s configuration. | [connector](#connector) | +| status | The updated case status, which can be: `open` or `closed`. | string | +| version | The current case version. | string | + +#### `subActionParams (addComment)` + +| Property | Description | Type | +| -------- | --------------------------------------------------------- | ------ | +| comment | The case’s new comment. | string | +| type | The type of the comment, which can be: `user` or `alert`. | string | + +#### `connector` + +| Property | Description | Type | +| -------- | ------------------------------------------------------------------------------------------------- | ----------------- | +| id | ID of the connector used for pushing case updates to external systems. | string | +| name | The connector name. | string | +| type | The type of the connector. Must be one of these: `.servicenow`, `jira`, `.resilient`, and `.none` | string | +| fields | Object containing the connector’s fields. | [fields](#fields) | + +#### `fields` + +For ServiceNow connectors: + +| Property | Description | Type | +| -------- | ----------------------------- | ------ | +| urgency | The urgency of the incident. | string | +| severity | The severity of the incident. | string | +| impact | The impact of the incident. | string | + +For Jira connectors: + +| Property | Description | Type | +| --------- | -------------------------------------------------------------------- | ------ | +| issueType | The issue type of the issue. | string | +| priority | The priority of the issue. | string | +| parent | The key of the parent issue (Valid when the issue type is Sub-task). | string | + +For IBM Resilient connectors: + +| Property | Description | Type | +| ------------ | ------------------------------- | -------- | +| issueTypes | The issue types of the issue. | string[] | +| severityCode | The severity code of the issue. | string | + +--- + # Command Line Utility The [`kbn-action`](https://github.com/pmuellr/kbn-action) tool can be used to send HTTP requests to the Actions plugin. For instance, to create a Slack action from the `.slack` Action Type, use the following command: From d044138edc3ab471801513c0f8c489748a9c7d59 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 2 Nov 2020 20:02:38 +0200 Subject: [PATCH 16/23] Improve casting on executor --- .../plugins/case/server/connectors/case/index.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/case/server/connectors/case/index.ts b/x-pack/plugins/case/server/connectors/case/index.ts index 6f0898e129147..897c63faabec4 100644 --- a/x-pack/plugins/case/server/connectors/case/index.ts +++ b/x-pack/plugins/case/server/connectors/case/index.ts @@ -45,13 +45,12 @@ export function getActionType({ } // action executor - async function executor( { logger, caseService, caseConfigureService, userActionService }: GetActionTypeParams, execOptions: CaseActionTypeExecutorOptions ): Promise> { const { actionId, params, services } = execOptions; - const { subAction, subActionParams } = params as CaseExecutorParams; + const { subAction, subActionParams } = params; let data: CaseExecutorResponse | null = null; const { savedObjectsClient } = services; @@ -70,14 +69,11 @@ async function executor( } if (subAction === 'create') { - const createParams = subActionParams as ExecutorSubActionCreateParams; - - data = await caseClient.create({ theCase: createParams as CasePostRequest }); + data = await caseClient.create({ theCase: subActionParams as CasePostRequest }); } if (subAction === 'update') { - const updateParams = subActionParams as ExecutorSubActionUpdateParams; - const updateParamsWithoutNullValues = Object.entries(updateParams).reduce( + const updateParamsWithoutNullValues = Object.entries(subActionParams).reduce( (acc, [key, value]) => ({ ...acc, ...(value != null ? { [key]: value } : {}), @@ -89,9 +85,7 @@ async function executor( } if (subAction === 'addComment') { - const addCommentParams = subActionParams as ExecutorSubActionAddCommentParams; - const { caseId, comment } = addCommentParams; - + const { caseId, comment } = subActionParams as ExecutorSubActionAddCommentParams; data = await caseClient.addComment({ caseId, comment }); } From c856aadc0e7440fac06d88d422f8fcc70471105a Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 2 Nov 2020 20:14:11 +0200 Subject: [PATCH 17/23] Translate name --- x-pack/plugins/case/server/connectors/case/index.ts | 6 ++---- .../case/server/connectors/case/translations.ts | 11 +++++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/case/server/connectors/case/translations.ts diff --git a/x-pack/plugins/case/server/connectors/case/index.ts b/x-pack/plugins/case/server/connectors/case/index.ts index 897c63faabec4..f284f0ed9668c 100644 --- a/x-pack/plugins/case/server/connectors/case/index.ts +++ b/x-pack/plugins/case/server/connectors/case/index.ts @@ -12,14 +12,12 @@ import { CasePatchRequest, CasePostRequest } from '../../../common/api'; import { createCaseClient } from '../../client'; import { CaseExecutorParamsSchema, CaseConfigurationSchema } from './schema'; import { - CaseExecutorParams, CaseExecutorResponse, - ExecutorSubActionCreateParams, - ExecutorSubActionUpdateParams, ExecutorSubActionAddCommentParams, CaseActionType, CaseActionTypeExecutorOptions, } from './types'; +import * as i18n from './translations'; import { GetActionTypeParams } from '..'; @@ -35,7 +33,7 @@ export function getActionType({ return { id: '.case', minimumLicenseRequired: 'gold', - name: 'Case', + name: i18n.NAME, validate: { config: CaseConfigurationSchema, params: CaseExecutorParamsSchema, diff --git a/x-pack/plugins/case/server/connectors/case/translations.ts b/x-pack/plugins/case/server/connectors/case/translations.ts new file mode 100644 index 0000000000000..9356ea8a31797 --- /dev/null +++ b/x-pack/plugins/case/server/connectors/case/translations.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const NAME = i18n.translate('xpack.case.connectors.case.title', { + defaultMessage: 'Case', +}); From 37d5f4e158e4d5a2527f98efc5c5644e1c06ff28 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 2 Nov 2020 20:16:07 +0200 Subject: [PATCH 18/23] Improve test --- .../case/server/connectors/case/index.test.ts | 132 ++++++++++-------- 1 file changed, 76 insertions(+), 56 deletions(-) diff --git a/x-pack/plugins/case/server/connectors/case/index.test.ts b/x-pack/plugins/case/server/connectors/case/index.test.ts index 4c1e0a01ef8da..1911b921f8535 100644 --- a/x-pack/plugins/case/server/connectors/case/index.test.ts +++ b/x-pack/plugins/case/server/connectors/case/index.test.ts @@ -78,72 +78,92 @@ describe('case connector', () => { }); describe('connector', () => { - it('succeeds when jira fields are valid', () => { - const params: Record = { - subAction: 'create', - subActionParams: { - title: 'Case from case connector!!', - tags: ['case', 'connector'], - description: 'Yo fields!!', - connector: { - id: 'jira', - name: 'Jira', - type: '.jira', - fields: { - issueType: '10006', - priority: 'High', - parent: null, + const connectorTests = [ + { + test: 'jira', + params: { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + tags: ['case', 'connector'], + description: 'Yo fields!!', + connector: { + id: 'jira', + name: 'Jira', + type: '.jira', + fields: { + issueType: '10006', + priority: 'High', + parent: null, + }, }, }, }, - }; - - expect(validateParams(caseActionType, params)).toEqual(params); - }); - - it('succeeds when resilient fields are valid', () => { - const params: Record = { - subAction: 'create', - subActionParams: { - title: 'Case from case connector!!', - tags: ['case', 'connector'], - description: 'Yo fields!!', - connector: { - id: 'resilient', - name: 'Resilient', - type: '.resilient', - fields: { - incidentTypes: ['13'], - severityCode: '3', + }, + { + test: 'resilient', + params: { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + tags: ['case', 'connector'], + description: 'Yo fields!!', + connector: { + id: 'resilient', + name: 'Resilient', + type: '.resilient', + fields: { + incidentTypes: ['13'], + severityCode: '3', + }, }, }, }, - }; - - expect(validateParams(caseActionType, params)).toEqual(params); - }); - - it('succeeds when servicenow fields are valid', () => { - const params: Record = { - subAction: 'create', - subActionParams: { - title: 'Case from case connector!!', - tags: ['case', 'connector'], - description: 'Yo fields!!', - connector: { - id: 'servicenow', - name: 'Servicenow', - type: '.servicenow', - fields: { - impact: 'Medium', - severity: 'Medium', - urgency: 'Medium', + }, + { + test: 'servicenow', + params: { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + tags: ['case', 'connector'], + description: 'Yo fields!!', + connector: { + id: 'servicenow', + name: 'Servicenow', + type: '.servicenow', + fields: { + impact: 'Medium', + severity: 'Medium', + urgency: 'Medium', + }, }, }, }, - }; + }, + { + test: 'none', + params: { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + tags: ['case', 'connector'], + description: 'Yo fields!!', + connector: { + id: 'none', + name: 'None', + type: '.none', + fields: null, + }, + }, + }, + }, + ]; - expect(validateParams(caseActionType, params)).toEqual(params); + connectorTests.forEach(({ params, test }) => { + it(`succeeds when ${test} fields are valid`, () => { + expect(validateParams(caseActionType, params)).toEqual(params); + }); }); it('set fields to null if they are missing', () => { From 166d4374b224f3dda03d81081116d05f98ad23cd Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 2 Nov 2020 20:29:02 +0200 Subject: [PATCH 19/23] Create comment type enum --- x-pack/plugins/case/common/api/cases/comment.ts | 5 +++++ .../case/server/client/comments/add.test.ts | 17 +++++++++-------- .../case/server/connectors/case/index.test.ts | 10 +++++----- .../api/__fixtures__/mock_saved_objects.ts | 7 ++++--- .../api/cases/comments/post_comment.test.ts | 9 +++++---- .../case/server/routes/api/utils.test.ts | 14 +++++++------- x-pack/plugins/case/server/routes/api/utils.ts | 3 ++- .../server/saved_object_types/migrations.ts | 6 +++--- .../cases/components/add_comment/index.test.tsx | 4 ++-- .../cases/components/add_comment/index.tsx | 6 +++--- .../public/cases/containers/api.test.tsx | 4 ++-- .../public/cases/containers/mock.ts | 5 +++-- .../public/cases/containers/types.ts | 10 ++++++++-- .../cases/containers/use_post_comment.test.tsx | 4 +++- 14 files changed, 61 insertions(+), 43 deletions(-) diff --git a/x-pack/plugins/case/common/api/cases/comment.ts b/x-pack/plugins/case/common/api/cases/comment.ts index 2bc319bcb40b5..b4daac93940d8 100644 --- a/x-pack/plugins/case/common/api/cases/comment.ts +++ b/x-pack/plugins/case/common/api/cases/comment.ts @@ -49,6 +49,11 @@ export const CommentsResponseRt = rt.type({ total: rt.number, }); +export enum CommentType { + user = 'user', + alert = 'alert', +} + export const AllCommentsResponseRt = rt.array(CommentResponseRt); export type CommentAttributes = rt.TypeOf; diff --git a/x-pack/plugins/case/server/client/comments/add.test.ts b/x-pack/plugins/case/server/client/comments/add.test.ts index 49be8920744c0..50e104b30178a 100644 --- a/x-pack/plugins/case/server/client/comments/add.test.ts +++ b/x-pack/plugins/case/server/client/comments/add.test.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { CommentType } from '../../../common/api'; import { createMockSavedObjectsRepository, mockCaseComments, @@ -30,14 +31,14 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); const res = await caseClient.client.addComment({ caseId: 'mock-id-1', - comment: { comment: 'Wow, good luck catching that bad meanie!', type: 'user' }, + comment: { comment: 'Wow, good luck catching that bad meanie!', type: CommentType.user }, }); expect(res.id).toEqual('mock-id-1'); expect(res.totalComment).toEqual(res.comments!.length); expect(res.comments![res.comments!.length - 1]).toEqual({ comment: 'Wow, good luck catching that bad meanie!', - type: 'user', + type: CommentType.user, created_at: '2020-10-23T21:54:48.952Z', created_by: { email: 'd00d@awesome.com', @@ -62,7 +63,7 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); const res = await caseClient.client.addComment({ caseId: 'mock-id-1', - comment: { comment: 'Wow, good luck catching that bad meanie!', type: 'user' }, + comment: { comment: 'Wow, good luck catching that bad meanie!', type: CommentType.user }, }); expect(res.updated_at).toEqual('2020-10-23T21:54:48.952Z'); @@ -82,7 +83,7 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); await caseClient.client.addComment({ caseId: 'mock-id-1', - comment: { comment: 'Wow, good luck catching that bad meanie!', type: 'user' }, + comment: { comment: 'Wow, good luck catching that bad meanie!', type: CommentType.user }, }); expect( @@ -126,13 +127,13 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient, true); const res = await caseClient.client.addComment({ caseId: 'mock-id-1', - comment: { comment: 'Wow, good luck catching that bad meanie!', type: 'user' }, + comment: { comment: 'Wow, good luck catching that bad meanie!', type: CommentType.user }, }); expect(res.id).toEqual('mock-id-1'); expect(res.comments![res.comments!.length - 1]).toEqual({ comment: 'Wow, good luck catching that bad meanie!', - type: 'user', + type: CommentType.user, created_at: '2020-10-23T21:54:48.952Z', created_by: { email: null, @@ -203,7 +204,7 @@ describe('addComment', () => { caseClient.client .addComment({ caseId: 'not-exists', - comment: { comment: 'Wow, good luck catching that bad meanie!', type: 'user' }, + comment: { comment: 'Wow, good luck catching that bad meanie!', type: CommentType.user }, }) .catch((e) => { expect(e).not.toBeNull(); @@ -223,7 +224,7 @@ describe('addComment', () => { caseClient.client .addComment({ caseId: 'mock-id-1', - comment: { comment: 'Throw an error', type: 'user' }, + comment: { comment: 'Throw an error', type: CommentType.user }, }) .catch((e) => { expect(e).not.toBeNull(); diff --git a/x-pack/plugins/case/server/connectors/case/index.test.ts b/x-pack/plugins/case/server/connectors/case/index.test.ts index 1911b921f8535..e14281e047915 100644 --- a/x-pack/plugins/case/server/connectors/case/index.test.ts +++ b/x-pack/plugins/case/server/connectors/case/index.test.ts @@ -8,7 +8,7 @@ import { Logger } from '../../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { actionsMock } from '../../../../actions/server/mocks'; import { validateParams } from '../../../../actions/server/lib'; -import { ConnectorTypes } from '../../../common/api'; +import { ConnectorTypes, CommentType } from '../../../common/api'; import { createCaseServiceMock, createConfigureServiceMock, @@ -619,7 +619,7 @@ describe('case connector', () => { subAction: 'addComment', subActionParams: { caseId: 'case-id', - comment: { comment: 'a comment', type: 'user' }, + comment: { comment: 'a comment', type: CommentType.user }, }, }; @@ -842,7 +842,7 @@ describe('case connector', () => { comments: [ { comment: 'a comment', - type: 'user' as const, + type: CommentType.user as const, created_at: '2020-10-23T21:54:48.952Z', created_by: { email: 'd00d@awesome.com', @@ -866,7 +866,7 @@ describe('case connector', () => { subAction: 'addComment', subActionParams: { caseId: 'case-id', - comment: { comment: 'a comment', type: 'user' }, + comment: { comment: 'a comment', type: CommentType.user }, }, }; @@ -883,7 +883,7 @@ describe('case connector', () => { expect(result).toEqual({ actionId, status: 'ok', data: commentReturn }); expect(mockCaseClient.addComment).toHaveBeenCalledWith({ caseId: 'case-id', - comment: { comment: 'a comment', type: 'user' }, + comment: { comment: 'a comment', type: CommentType.user }, }); }); }); diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts index aef78c4f92cdb..9314ebb445820 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -10,6 +10,7 @@ import { CommentAttributes, ESCaseAttributes, ConnectorTypes, + CommentType, } from '../../../../common/api'; export const mockCases: Array> = [ @@ -207,7 +208,7 @@ export const mockCaseComments: Array> = [ id: 'mock-comment-1', attributes: { comment: 'Wow, good luck catching that bad meanie!', - type: 'user', + type: CommentType.user, created_at: '2019-11-25T21:55:00.177Z', created_by: { full_name: 'elastic', @@ -238,7 +239,7 @@ export const mockCaseComments: Array> = [ id: 'mock-comment-2', attributes: { comment: 'Well I decided to update my comment. So what? Deal with it.', - type: 'user', + type: CommentType.user, created_at: '2019-11-25T21:55:14.633Z', created_by: { full_name: 'elastic', @@ -270,7 +271,7 @@ export const mockCaseComments: Array> = [ id: 'mock-comment-3', attributes: { comment: 'Wow, good luck catching that bad meanie!', - type: 'user', + type: CommentType.user, created_at: '2019-11-25T22:32:30.608Z', created_by: { full_name: 'elastic', diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts index 784d9f9796456..0b733bb034f8c 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts @@ -16,6 +16,7 @@ import { } from '../../__fixtures__'; import { initPostCommentApi } from './post_comment'; import { CASE_COMMENTS_URL } from '../../../../../common/constants'; +import { CommentType } from '../../../../../common/api'; describe('POST comment', () => { let routeHandler: RequestHandler; @@ -36,7 +37,7 @@ describe('POST comment', () => { }, body: { comment: 'Wow, good luck catching that bad meanie!', - type: 'user', + type: CommentType.user, }, }); @@ -63,7 +64,7 @@ describe('POST comment', () => { }, body: { comment: 'Wow, good luck catching that bad meanie!', - type: 'user', + type: CommentType.user, }, }); @@ -114,7 +115,7 @@ describe('POST comment', () => { }, body: { comment: 'Wow, good luck catching that bad meanie!', - type: 'user', + type: CommentType.user, }, }); @@ -130,7 +131,7 @@ describe('POST comment', () => { expect(response.status).toEqual(200); expect(response.payload.comments[response.payload.comments.length - 1]).toEqual({ comment: 'Wow, good luck catching that bad meanie!', - type: 'user', + type: CommentType.user, created_at: '2019-11-25T21:54:48.952Z', created_by: { email: null, diff --git a/x-pack/plugins/case/server/routes/api/utils.test.ts b/x-pack/plugins/case/server/routes/api/utils.test.ts index 3b03f9490f9c5..fc1086b03814b 100644 --- a/x-pack/plugins/case/server/routes/api/utils.test.ts +++ b/x-pack/plugins/case/server/routes/api/utils.test.ts @@ -23,7 +23,7 @@ import { mockCaseComments, mockCaseNoConnectorId, } from './__fixtures__/mock_saved_objects'; -import { ConnectorTypes, ESCaseConnector } from '../../../common/api'; +import { ConnectorTypes, ESCaseConnector, CommentType } from '../../../common/api'; describe('Utils', () => { describe('transformNewCase', () => { @@ -117,7 +117,7 @@ describe('Utils', () => { it('transforms correctly', () => { const comment = { comment: 'A comment', - type: 'user' as const, + type: CommentType.user, createdDate: '2020-04-09T09:43:51.778Z', email: 'elastic@elastic.co', full_name: 'Elastic', @@ -127,7 +127,7 @@ describe('Utils', () => { const res = transformNewComment(comment); expect(res).toEqual({ comment: 'A comment', - type: 'user', + type: CommentType.user, created_at: '2020-04-09T09:43:51.778Z', created_by: { email: 'elastic@elastic.co', full_name: 'Elastic', username: 'elastic' }, pushed_at: null, @@ -140,7 +140,7 @@ describe('Utils', () => { it('transform correctly without optional fields', () => { const comment = { comment: 'A comment', - type: 'user' as const, + type: CommentType.user, createdDate: '2020-04-09T09:43:51.778Z', }; @@ -148,7 +148,7 @@ describe('Utils', () => { expect(res).toEqual({ comment: 'A comment', - type: 'user', + type: CommentType.user, created_at: '2020-04-09T09:43:51.778Z', created_by: { email: undefined, full_name: undefined, username: undefined }, pushed_at: null, @@ -161,7 +161,7 @@ describe('Utils', () => { it('transform correctly with optional fields as null', () => { const comment = { comment: 'A comment', - type: 'user' as const, + type: CommentType.user, createdDate: '2020-04-09T09:43:51.778Z', email: null, full_name: null, @@ -172,7 +172,7 @@ describe('Utils', () => { expect(res).toEqual({ comment: 'A comment', - type: 'user', + type: CommentType.user, created_at: '2020-04-09T09:43:51.778Z', created_by: { email: null, full_name: null, username: null }, pushed_at: null, diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts index a5ee0421affd0..088b1adf90731 100644 --- a/x-pack/plugins/case/server/routes/api/utils.ts +++ b/x-pack/plugins/case/server/routes/api/utils.ts @@ -22,6 +22,7 @@ import { CommentAttributes, ESCaseConnector, ESCaseAttributes, + CommentType, } from '../../../common/api'; import { transformESConnectorToCaseConnector } from './cases/helpers'; @@ -57,7 +58,7 @@ export const transformNewCase = ({ interface NewCommentArgs { comment: string; - type: 'user' | 'alert'; + type: CommentType; createdDate: string; email?: string | null; full_name?: string | null; diff --git a/x-pack/plugins/case/server/saved_object_types/migrations.ts b/x-pack/plugins/case/server/saved_object_types/migrations.ts index 2bd8adfca6653..27c363a40af37 100644 --- a/x-pack/plugins/case/server/saved_object_types/migrations.ts +++ b/x-pack/plugins/case/server/saved_object_types/migrations.ts @@ -7,7 +7,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { SavedObjectUnsanitizedDoc, SavedObjectSanitizedDoc } from '../../../../../src/core/server'; -import { ConnectorTypes } from '../../common/api/connectors'; +import { ConnectorTypes, CommentType } from '../../common/api'; interface UnsanitizedCase { connector_id: string; @@ -133,7 +133,7 @@ interface UnsanitizedComment { interface SanitizedComment { comment: string; - type: 'user' | 'alert'; + type: CommentType; } export const commentsMigrations = { @@ -144,7 +144,7 @@ export const commentsMigrations = { ...doc, attributes: { ...doc.attributes, - type: 'user', + type: CommentType.user, }, references: doc.references || [], }; diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx index c270723bb18c6..2c8051f902b17 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx @@ -12,7 +12,7 @@ import { TestProviders } from '../../../common/mock'; import { getFormMock } from '../__mock__/form'; import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; -import { CommentRequest } from '../../../../../case/common/api'; +import { CommentRequest, CommentType } from '../../../../../case/common/api'; import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'; import { usePostComment } from '../../containers/use_post_comment'; import { useForm } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; @@ -69,7 +69,7 @@ const defaultPostCommment = { const sampleData: CommentRequest = { comment: 'what a cool comment', - type: 'user', + type: CommentType.user, }; describe('AddComment ', () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx index b6cc9e59d7815..c54bd8b621d83 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx @@ -8,7 +8,7 @@ import { EuiButton, EuiLoadingSpinner } from '@elastic/eui'; import React, { useCallback, forwardRef, useImperativeHandle } from 'react'; import styled from 'styled-components'; -import { CommentRequest } from '../../../../../case/common/api'; +import { CommentRequest, CommentType } from '../../../../../case/common/api'; import { usePostComment } from '../../containers/use_post_comment'; import { Case } from '../../containers/types'; import { MarkdownEditorForm } from '../../../common/components/markdown_editor/eui_form'; @@ -27,7 +27,7 @@ const MySpinner = styled(EuiLoadingSpinner)` const initialCommentValue: CommentRequest = { comment: '', - type: 'user', + type: CommentType.user, }; export interface AddCommentRefObject { @@ -82,7 +82,7 @@ export const AddComment = React.memo( if (onCommentSaving != null) { onCommentSaving(); } - postComment({ ...data, type: 'user' }, onCommentPosted); + postComment({ ...data, type: CommentType.user }, onCommentPosted); reset(); } }, [onCommentPosted, onCommentSaving, postComment, reset, submit]); diff --git a/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx index 82ac9936acb7c..0d5bf13cd6261 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx @@ -51,7 +51,7 @@ import { import { DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './use_get_cases'; import * as i18n from './translations'; -import { ConnectorTypes } from '../../../../case/common/api/connectors'; +import { ConnectorTypes, CommentType } from '../../../../case/common/api'; const abortCtrl = new AbortController(); const mockKibanaServices = KibanaServices.get as jest.Mock; @@ -404,7 +404,7 @@ describe('Case Configuration API', () => { }); const data = { comment: 'comment', - type: 'user' as const, + type: CommentType.user, }; test('check url, method, signal', async () => { diff --git a/x-pack/plugins/security_solution/public/cases/containers/mock.ts b/x-pack/plugins/security_solution/public/cases/containers/mock.ts index d3b13f8b89961..c5b60041f5cac 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/mock.ts @@ -17,7 +17,8 @@ import { CaseUserActionsResponse, CasesResponse, CasesFindResponse, -} from '../../../../case/common/api/cases'; + CommentType, +} from '../../../../case/common/api'; import { UseGetCasesState, DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './use_get_cases'; import { ConnectorTypes } from '../../../../case/common/api/connectors'; export { connectorsMock } from './configure/mock'; @@ -42,7 +43,7 @@ export const tags: string[] = ['coke', 'pepsi']; export const basicComment: Comment = { comment: 'Solve this fast!', - type: 'user' as const, + type: CommentType.user, id: basicCommentId, createdAt: basicCreatedAt, createdBy: elasticUser, diff --git a/x-pack/plugins/security_solution/public/cases/containers/types.ts b/x-pack/plugins/security_solution/public/cases/containers/types.ts index 460dfa7088d0c..c2ddcce8b1d3c 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/types.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/types.ts @@ -4,7 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { User, UserActionField, UserAction, CaseConnector } from '../../../../case/common/api'; +import { + User, + UserActionField, + UserAction, + CaseConnector, + CommentType, +} from '../../../../case/common/api'; export { CaseConnector, ActionConnector } from '../../../../case/common/api'; @@ -13,7 +19,7 @@ export interface Comment { createdAt: string; createdBy: ElasticUser; comment: string; - type: 'user' | 'alert'; + type: CommentType; pushedAt: string | null; pushedBy: string | null; updatedAt: string | null; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx index b41912ac9d0b6..773d4b8d1fe56 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx @@ -5,6 +5,8 @@ */ import { renderHook, act } from '@testing-library/react-hooks'; + +import { CommentType } from '../../../../case/common/api'; import { usePostComment, UsePostComment } from './use_post_comment'; import { basicCaseId } from './mock'; import * as api from './api'; @@ -15,7 +17,7 @@ describe('usePostComment', () => { const abortCtrl = new AbortController(); const samplePost = { comment: 'a comment', - type: 'user' as const, + type: CommentType.user, }; const updateCaseCallback = jest.fn(); beforeEach(() => { From 3d66246202f502c8aa8153cba3468f7c12823a2e Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 3 Nov 2020 11:06:53 +0200 Subject: [PATCH 20/23] Fix type --- x-pack/plugins/case/server/routes/api/utils.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts index 088b1adf90731..f8fe149c2ff2f 100644 --- a/x-pack/plugins/case/server/routes/api/utils.ts +++ b/x-pack/plugins/case/server/routes/api/utils.ts @@ -22,7 +22,7 @@ import { CommentAttributes, ESCaseConnector, ESCaseAttributes, - CommentType, + CommentRequest, } from '../../../common/api'; import { transformESConnectorToCaseConnector } from './cases/helpers'; @@ -56,14 +56,13 @@ export const transformNewCase = ({ updated_by: null, }); -interface NewCommentArgs { - comment: string; - type: CommentType; +interface NewCommentArgs extends CommentRequest { createdDate: string; email?: string | null; full_name?: string | null; username?: string | null; } + export const transformNewComment = ({ comment, type, From 10e233f0c8f3950ca54f9beece7a083feaec4f43 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 3 Nov 2020 11:14:15 +0200 Subject: [PATCH 21/23] Fix i18n --- x-pack/.i18nrc.json | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 8993213d91f23..8ed7535f511ad 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -9,6 +9,7 @@ "xpack.apm": "plugins/apm", "xpack.beatsManagement": "plugins/beats_management", "xpack.canvas": "plugins/canvas", + "xpack.case": "plugins/case", "xpack.cloud": "plugins/cloud", "xpack.dashboard": "plugins/dashboard_enhanced", "xpack.discover": "plugins/discover_enhanced", From e010cb8cca8fa4c20a6f94f81bbd3f17bfce8edb Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 3 Nov 2020 15:16:18 +0200 Subject: [PATCH 22/23] Move README to cases --- x-pack/plugins/actions/README.md | 122 +++++++------------------------ x-pack/plugins/case/README.md | 88 ++++++++++++++++++++++ 2 files changed, 115 insertions(+), 95 deletions(-) diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 82c0870ae496e..4fef9bc582d08 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -1,3 +1,4 @@ +# Kibana Actions The Kibana actions plugin provides a framework to create executable actions. You can: @@ -13,6 +14,31 @@ The Kibana actions plugin provides a framework to create executable actions. You Table of Contents +- [Kibana Actions](#kibana-actions) + - [Terminology](#terminology) + - [Usage](#usage) + - [Kibana Actions Configuration](#kibana-actions-configuration) + - [Configuration Options](#configuration-options) + - [Adding Built-in Action Types to allowedHosts](#adding-built-in-action-types-to-allowedhosts) + - [Configuration Utilities](#configuration-utilities) + - [Action types](#action-types) + - [Methods](#methods) + - [Executor](#executor) + - [Example](#example) + - [RESTful API](#restful-api) + - [`POST /api/actions/action`: Create action](#post-apiactionsaction-create-action) + - [`DELETE /api/actions/action/{id}`: Delete action](#delete-apiactionsactionid-delete-action) + - [`GET /api/actions`: Get all actions](#get-apiactions-get-all-actions) + - [`GET /api/actions/action/{id}`: Get action](#get-apiactionsactionid-get-action) + - [`GET /api/actions/list_action_types`: List action types](#get-apiactionslist_action_types-list-action-types) + - [`PUT /api/actions/action/{id}`: Update action](#put-apiactionsactionid-update-action) + - [`POST /api/actions/action/{id}/_execute`: Execute action](#post-apiactionsactionid_execute-execute-action) + - [Firing actions](#firing-actions) + - [Accessing a scoped ActionsClient](#accessing-a-scoped-actionsclient) + - [actionsClient.enqueueExecution(options)](#actionsclientenqueueexecutionoptions) + - [Example](#example-1) + - [actionsClient.execute(options)](#actionsclientexecuteoptions) + - [Example](#example-2) - [Built-in Action Types](#built-in-action-types) - [Server log](#server-log) - [`config`](#config) @@ -55,15 +81,6 @@ Table of Contents - [`secrets`](#secrets-8) - [`params`](#params-8) - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-3) - - [Case](#case) - - [`config`](#config-9) - - [`secrets`](#secrets-9) - - [`params`](#params-9) - - [`subActionParams (create)`](#subactionparams-create) - - [`subActionParams (update)`](#subactionparams-update) - - [`subActionParams (addComment)`](#subactionparams-addcomment) - - [`connector`](#connector) - - [`fields`](#fields) - [Command Line Utility](#command-line-utility) - [Developing New Action Types](#developing-new-action-types) - [licensing](#licensing) @@ -653,91 +670,6 @@ ID: `.resilient` | incidentTypes | An array with the ids of IBM Resilient incident types. | number[] _(optional)_ | | severityCode | IBM Resilient id of the severity code. | number _(optional)_ | -## Case - -ID: `.case` - -The params properties are modelled after the arguments to the [Cases API](https://www.elastic.co/guide/en/security/master/cases-api-overview.html). - -### `config` - -This action has no `config` properties. - -### `secrets` - -This action type has no `secrets` properties. - -### `params` - -| Property | Description | Type | -| --------------- | ------------------------------------------------------------------------- | ------ | -| subAction | The sub action to perform. It can be `create`, `update`, and `addComment` | string | -| subActionParams | The parameters of the sub action | object | - -#### `subActionParams (create)` - -| Property | Description | Type | -| ----------- | --------------------------------------------------------------------- | ----------------------- | -| tile | The case’s title. | string | -| description | The case’s description. | string | -| tags | String array containing words and phrases that help categorize cases. | string[] | -| connector | Object containing the connector’s configuration. | [connector](#connector) | - -#### `subActionParams (update)` - -| Property | Description | Type | -| ----------- | ---------------------------------------------------------- | ----------------------- | -| id | The ID of the case being updated. | string | -| tile | The updated case title. | string | -| description | The updated case description. | string | -| tags | The updated case tags. | string | -| connector | Object containing the connector’s configuration. | [connector](#connector) | -| status | The updated case status, which can be: `open` or `closed`. | string | -| version | The current case version. | string | - -#### `subActionParams (addComment)` - -| Property | Description | Type | -| -------- | --------------------------------------------------------- | ------ | -| comment | The case’s new comment. | string | -| type | The type of the comment, which can be: `user` or `alert`. | string | - -#### `connector` - -| Property | Description | Type | -| -------- | ------------------------------------------------------------------------------------------------- | ----------------- | -| id | ID of the connector used for pushing case updates to external systems. | string | -| name | The connector name. | string | -| type | The type of the connector. Must be one of these: `.servicenow`, `jira`, `.resilient`, and `.none` | string | -| fields | Object containing the connector’s fields. | [fields](#fields) | - -#### `fields` - -For ServiceNow connectors: - -| Property | Description | Type | -| -------- | ----------------------------- | ------ | -| urgency | The urgency of the incident. | string | -| severity | The severity of the incident. | string | -| impact | The impact of the incident. | string | - -For Jira connectors: - -| Property | Description | Type | -| --------- | -------------------------------------------------------------------- | ------ | -| issueType | The issue type of the issue. | string | -| priority | The priority of the issue. | string | -| parent | The key of the parent issue (Valid when the issue type is Sub-task). | string | - -For IBM Resilient connectors: - -| Property | Description | Type | -| ------------ | ------------------------------- | -------- | -| issueTypes | The issue types of the issue. | string[] | -| severityCode | The severity code of the issue. | string | - ---- - # Command Line Utility The [`kbn-action`](https://github.com/pmuellr/kbn-action) tool can be used to send HTTP requests to the Actions plugin. For instance, to create a Slack action from the `.slack` Action Type, use the following command: @@ -792,4 +724,4 @@ Instead of `schema.maybe()`, use `schema.nullable()`, which is the same as `sche ## user interface -In order to make this action usable in the Kibana UI, you will need to provide all the UI editing aspects of the action. The existing action type user interfaces are defined in [`x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types`](../triggers_actions_ui/public/application/components/builtin_action_types). For more information, see the [UI documentation](../triggers_actions_ui/README.md#create-and-register-new-action-type-ui). +In order to make this action usable in the Kibana UI, you will need to provide all the UI editing aspects of the action. The existing action type user interfaces are defined in [`x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types`](../triggers_actions_ui/public/application/components/builtin_action_types). For more information, see the [UI documentation](../triggers_actions_ui/README.md#create-and-register-new-action-type-ui). \ No newline at end of file diff --git a/x-pack/plugins/case/README.md b/x-pack/plugins/case/README.md index c0acb87835207..002fbfb8b53f7 100644 --- a/x-pack/plugins/case/README.md +++ b/x-pack/plugins/case/README.md @@ -7,3 +7,91 @@ Elastic is developing a Case Management Workflow. Follow our progress: - [Case API Documentation](https://documenter.getpostman.com/view/172706/SW7c2SuF?version=latest) - [Github Meta](https://github.com/elastic/kibana/issues/50103) + +# Action types + + +See [Kibana Actions](https://github.com/elastic/kibana/tree/master/x-pack/plugins/actions) for more information. + +## Case + +ID: `.case` + +The params properties are modelled after the arguments to the [Cases API](https://www.elastic.co/guide/en/security/master/cases-api-overview.html). + +### `config` + +This action has no `config` properties. + +### `secrets` + +This action type has no `secrets` properties. + +### `params` + +| Property | Description | Type | +| --------------- | ------------------------------------------------------------------------- | ------ | +| subAction | The sub action to perform. It can be `create`, `update`, and `addComment` | string | +| subActionParams | The parameters of the sub action | object | + +#### `subActionParams (create)` + +| Property | Description | Type | +| ----------- | --------------------------------------------------------------------- | ----------------------- | +| tile | The case’s title. | string | +| description | The case’s description. | string | +| tags | String array containing words and phrases that help categorize cases. | string[] | +| connector | Object containing the connector’s configuration. | [connector](#connector) | + +#### `subActionParams (update)` + +| Property | Description | Type | +| ----------- | ---------------------------------------------------------- | ----------------------- | +| id | The ID of the case being updated. | string | +| tile | The updated case title. | string | +| description | The updated case description. | string | +| tags | The updated case tags. | string | +| connector | Object containing the connector’s configuration. | [connector](#connector) | +| status | The updated case status, which can be: `open` or `closed`. | string | +| version | The current case version. | string | + +#### `subActionParams (addComment)` + +| Property | Description | Type | +| -------- | --------------------------------------------------------- | ------ | +| comment | The case’s new comment. | string | +| type | The type of the comment, which can be: `user` or `alert`. | string | + +#### `connector` + +| Property | Description | Type | +| -------- | ------------------------------------------------------------------------------------------------- | ----------------- | +| id | ID of the connector used for pushing case updates to external systems. | string | +| name | The connector name. | string | +| type | The type of the connector. Must be one of these: `.servicenow`, `jira`, `.resilient`, and `.none` | string | +| fields | Object containing the connector’s fields. | [fields](#fields) | + +#### `fields` + +For ServiceNow connectors: + +| Property | Description | Type | +| -------- | ----------------------------- | ------ | +| urgency | The urgency of the incident. | string | +| severity | The severity of the incident. | string | +| impact | The impact of the incident. | string | + +For Jira connectors: + +| Property | Description | Type | +| --------- | -------------------------------------------------------------------- | ------ | +| issueType | The issue type of the issue. | string | +| priority | The priority of the issue. | string | +| parent | The key of the parent issue (Valid when the issue type is Sub-task). | string | + +For IBM Resilient connectors: + +| Property | Description | Type | +| ------------ | ------------------------------- | -------- | +| issueTypes | The issue types of the issue. | string[] | +| severityCode | The severity code of the issue. | string | From da442fc69c2e9caea2bad0efde70f369ff688b67 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 3 Nov 2020 22:37:03 +0200 Subject: [PATCH 23/23] Filter out case connector from alerting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mike Côté --- .../sections/action_connector_form/action_form.tsx | 7 ++++++- .../triggers_actions_ui/public/common/constants/index.ts | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 51d3b0074ca54..74432157f5659 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -45,7 +45,7 @@ import { SectionLoading } from '../../components/section_loading'; import { ConnectorAddModal } from './connector_add_modal'; import { actionTypeCompare } from '../../lib/action_type_compare'; import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled'; -import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants'; +import { VIEW_LICENSE_OPTIONS_LINK, DEFAULT_HIDDEN_ACTION_TYPES } from '../../../common/constants'; import { hasSaveActionsCapability } from '../../lib/capabilities'; interface ActionAccordionFormProps { @@ -579,6 +579,11 @@ export const ActionForm = ({ const preconfiguredConnectors = connectors.filter((connector) => connector.isPreconfigured); actionTypeNodes = actionTypeRegistry .list() + /** + * TODO: Remove when cases connector is available across Kibana. Issue: https://github.com/elastic/kibana/issues/82502. + * If actionTypes are set, hidden connectors are filtered out. Otherwise, they are not. + */ + .filter(({ id }) => actionTypes ?? !DEFAULT_HIDDEN_ACTION_TYPES.includes(id)) .filter((item) => actionTypesIndex[item.id]) .filter((item) => !!item.actionParamsFields) .sort((a, b) => diff --git a/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts index a2a1657a1f4cc..833ed915fad59 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts @@ -9,3 +9,5 @@ export { AGGREGATION_TYPES, builtInAggregationTypes } from './aggregation_types' export { builtInGroupByTypes } from './group_by_types'; export const VIEW_LICENSE_OPTIONS_LINK = 'https://www.elastic.co/subscriptions'; +// TODO: Remove when cases connector is available across Kibana. Issue: https://github.com/elastic/kibana/issues/82502. +export const DEFAULT_HIDDEN_ACTION_TYPES = ['.case'];