From 7a47cf5636489500e32cf02f84cf3b60c6c086bb Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 28 Feb 2020 18:12:41 +0200 Subject: [PATCH 01/42] Refactor structure --- .../servicenow/constants.ts | 7 ++ .../index.test.ts} | 14 ++-- .../{servicenow.ts => servicenow/index.ts} | 77 ++++--------------- .../builtin_action_types/servicenow/schema.ts | 25 ++++++ .../servicenow/translations.ts | 63 +++++++++++++++ .../builtin_action_types/servicenow/types.ts | 17 ++++ 6 files changed, 135 insertions(+), 68 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/constants.ts rename x-pack/plugins/actions/server/builtin_action_types/{servicenow.test.ts => servicenow/index.test.ts} (96%) rename x-pack/plugins/actions/server/builtin_action_types/{servicenow.ts => servicenow/index.ts} (51%) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/constants.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/constants.ts new file mode 100644 index 0000000000000..32d3f29edfdf0 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/constants.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 const ACTION_TYPE_ID = '.servicenow'; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts similarity index 96% rename from x-pack/plugins/actions/server/builtin_action_types/servicenow.test.ts rename to x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts index 9ae96cb23a5c3..71a1141448523 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts @@ -8,13 +8,13 @@ jest.mock('./lib/post_servicenow', () => ({ postServiceNow: jest.fn(), })); -import { getActionType } from './servicenow'; -import { ActionType, Services, ActionTypeExecutorOptions } from '../types'; -import { validateConfig, validateSecrets, validateParams } from '../lib'; -import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; -import { postServiceNow } from './lib/post_servicenow'; -import { createActionTypeRegistry } from './index.test'; -import { configUtilsMock } from '../actions_config.mock'; +import { getActionType } from '.'; +import { ActionType, Services, ActionTypeExecutorOptions } from '../../types'; +import { validateConfig, validateSecrets, validateParams } from '../../lib'; +import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { postServiceNow } from '../lib/post_servicenow'; +import { createActionTypeRegistry } from '../index.test'; +import { configUtilsMock } from '../../actions_config.mock'; const postServiceNowMock = postServiceNow as jest.Mock; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts similarity index 51% rename from x-pack/plugins/actions/server/builtin_action_types/servicenow.ts rename to x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index 0ad435281eba4..926ba54c01f52 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -5,80 +5,49 @@ */ import { curry } from 'lodash'; -import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult, ExecutorType, -} from '../types'; -import { ActionsConfigurationUtilities } from '../actions_config'; -import { postServiceNow } from './lib/post_servicenow'; +} from '../../types'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { postServiceNow } from '../lib/post_servicenow'; -// config definition -export type ConfigType = TypeOf; +import * as i18n from './translations'; -const ConfigSchemaProps = { - apiUrl: schema.string(), -}; +import { ACTION_TYPE_ID } from './constants'; +import { ConfigType, SecretsType, ParamsType } from './types'; -const ConfigSchema = schema.object(ConfigSchemaProps); +import { ConfigSchemaProps, SecretsSchemaProps, ParamsSchema } from './schema'; function validateConfig( configurationUtilities: ActionsConfigurationUtilities, configObject: ConfigType ) { if (configObject.apiUrl == null) { - return i18n.translate('xpack.actions.builtin.servicenow.servicenowApiNullError', { - defaultMessage: 'ServiceNow [apiUrl] is required', - }); + return i18n.API_URL_REQUIRED; } try { configurationUtilities.ensureWhitelistedUri(configObject.apiUrl); } catch (whitelistError) { - return i18n.translate('xpack.actions.builtin.servicenow.servicenowApiWhitelistError', { - defaultMessage: 'error configuring servicenow action: {message}', - values: { - message: whitelistError.message, - }, - }); + return i18n.WHITE_LISTED_ERROR(whitelistError.message); } } -// secrets definition -export type SecretsType = TypeOf; -const SecretsSchemaProps = { - password: schema.string(), - username: schema.string(), -}; - -const SecretsSchema = schema.object(SecretsSchemaProps); function validateSecrets( configurationUtilities: ActionsConfigurationUtilities, secrets: SecretsType ) { if (secrets.username == null) { - return i18n.translate('xpack.actions.builtin.servicenow.servicenowApiUserError', { - defaultMessage: 'error configuring servicenow action: no secrets [username] provided', - }); + return i18n.NO_USERNAME; } if (secrets.password == null) { - return i18n.translate('xpack.actions.builtin.servicenow.servicenowApiPasswordError', { - defaultMessage: 'error configuring servicenow action: no secrets [password] provided', - }); + return i18n.NO_PASSWORD; } } -// params definition - -export type ParamsType = TypeOf; - -const ParamsSchema = schema.object({ - comments: schema.maybe(schema.string()), - short_description: schema.string(), -}); - // action type definition export function getActionType({ configurationUtilities, @@ -88,10 +57,8 @@ export function getActionType({ executor?: ExecutorType; }): ActionType { return { - id: '.servicenow', - name: i18n.translate('xpack.actions.builtin.servicenowTitle', { - defaultMessage: 'ServiceNow', - }), + id: ACTION_TYPE_ID, + name: i18n.NAME, validate: { config: schema.object(ConfigSchemaProps, { validate: curry(validateConfig)(configurationUtilities), @@ -122,9 +89,7 @@ async function serviceNowExecutor( try { response = await postServiceNow({ apiUrl: config.apiUrl, data: params, headers, secrets }); } catch (err) { - const message = i18n.translate('xpack.actions.builtin.servicenow.postingErrorMessage', { - defaultMessage: 'error posting servicenow event', - }); + const message = i18n.ERROR_POSTING; return { status: 'error', actionId, @@ -141,12 +106,7 @@ async function serviceNowExecutor( } if (response.status === 429 || response.status >= 500) { - const message = i18n.translate('xpack.actions.builtin.servicenow.postingRetryErrorMessage', { - defaultMessage: 'error posting servicenow event: http status {status}, retry later', - values: { - status: response.status, - }, - }); + const message = i18n.RETRY_POSTING(response.status); return { status: 'error', @@ -156,12 +116,7 @@ async function serviceNowExecutor( }; } - const message = i18n.translate('xpack.actions.builtin.servicenow.postingUnexpectedErrorMessage', { - defaultMessage: 'error posting servicenow event: unexpected status {status}', - values: { - status: response.status, - }, - }); + const message = i18n.UNEXPECTED_STATUS(response.status); return { status: 'error', diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts new file mode 100644 index 0000000000000..100b549958632 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts @@ -0,0 +1,25 @@ +/* + * 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'; + +export const ConfigSchemaProps = { + apiUrl: schema.string(), +}; + +export const ConfigSchema = schema.object(ConfigSchemaProps); + +export const SecretsSchemaProps = { + password: schema.string(), + username: schema.string(), +}; + +export const SecretsSchema = schema.object(SecretsSchemaProps); + +export const ParamsSchema = schema.object({ + comments: schema.maybe(schema.string()), + short_description: schema.string(), +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts new file mode 100644 index 0000000000000..1ad9ab17344a6 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts @@ -0,0 +1,63 @@ +/* + * 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 API_URL_REQUIRED = i18n.translate( + 'xpack.actions.builtin.servicenow.servicenowApiNullError', + { + defaultMessage: 'ServiceNow [apiUrl] is required', + } +); + +export const WHITE_LISTED_ERROR = (message: string) => + i18n.translate('xpack.actions.builtin.servicenow.servicenowApiWhitelistError', { + defaultMessage: 'error configuring servicenow action: {message}', + values: { + message, + }, + }); + +export const NO_USERNAME = i18n.translate( + 'xpack.actions.builtin.servicenow.servicenowApiUserError', + { + defaultMessage: 'error configuring servicenow action: no secrets [username] provided', + } +); + +export const NO_PASSWORD = i18n.translate( + 'xpack.actions.builtin.servicenow.servicenowApiPasswordError', + { + defaultMessage: 'error configuring servicenow action: no secrets [password] provided', + } +); + +export const NAME = i18n.translate('xpack.actions.builtin.servicenowTitle', { + defaultMessage: 'ServiceNow', +}); + +export const ERROR_POSTING = i18n.translate( + 'xpack.actions.builtin.servicenow.postingErrorMessage', + { + defaultMessage: 'error posting servicenow event', + } +); + +export const RETRY_POSTING = (status: number) => + i18n.translate('xpack.actions.builtin.servicenow.postingRetryErrorMessage', { + defaultMessage: 'error posting servicenow event: http status {status}, retry later', + values: { + status, + }, + }); + +export const UNEXPECTED_STATUS = (status: number) => + i18n.translate('xpack.actions.builtin.servicenow.postingUnexpectedErrorMessage', { + defaultMessage: 'error posting servicenow event: unexpected status {status}', + values: { + status, + }, + }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts new file mode 100644 index 0000000000000..ee37c8d8eac39 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -0,0 +1,17 @@ +/* + * 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 { ConfigSchema, SecretsSchema, ParamsSchema } from './schema'; + +// config definition +export type ConfigType = TypeOf; + +// secrets definition +export type SecretsType = TypeOf; + +export type ParamsType = TypeOf; From 4e940c103e7fcef7cb70d883070aa22e03bb68d4 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Sun, 1 Mar 2020 12:08:16 +0200 Subject: [PATCH 02/42] Init ServiceNow class --- .../lib/servicenow/index.ts | 22 +++++++++++++++++++ .../lib/servicenow/types.ts | 11 ++++++++++ 2 files changed, 33 insertions(+) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/types.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.ts new file mode 100644 index 0000000000000..29b19d3dc9942 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/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. + */ + +import { Instance } from './types'; + +class ServiceNow { + constructor(private readonly instance: Instance) { + if ( + !this.instance || + !this.instance.url || + !this.instance.username || + !this.instance.password + ) { + throw Error('[Action][ServiceNow]: Wrong configuration.'); + } + } +} + +export { ServiceNow }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/types.ts new file mode 100644 index 0000000000000..ab4d0af1b5e96 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/types.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. + */ + +export interface Instance { + url: string; + username: string; + password: string; +} From 3d098d3ac6946f6a3113b1293454db2d928e3f57 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Sun, 1 Mar 2020 12:18:48 +0200 Subject: [PATCH 03/42] Add constants --- .../actions/server/builtin_action_types/lib/post_servicenow.ts | 2 +- .../server/builtin_action_types/lib/servicenow/index.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/post_servicenow.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/post_servicenow.ts index cfd3a9d70dc93..795b649e42df7 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/post_servicenow.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/post_servicenow.ts @@ -6,7 +6,7 @@ import axios, { AxiosResponse } from 'axios'; import { Services } from '../../types'; -import { ParamsType, SecretsType } from '../servicenow'; +import { ParamsType, SecretsType } from '../servicenow/types'; interface PostServiceNowOptions { apiUrl: string; diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.ts index 29b19d3dc9942..308921c8fd50b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.ts @@ -7,6 +7,9 @@ import { Instance } from './types'; class ServiceNow { + private static readonly API_VERSION = 'v1'; + private static readonly INCIDENT_URL = `/api/now/${ServiceNow.API_VERSION}/table/incident`; + constructor(private readonly instance: Instance) { if ( !this.instance || From 58f6fd2d7970fb5351ebe010c419e557af7a72b0 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 2 Mar 2020 12:58:08 +0200 Subject: [PATCH 04/42] Add configuration scheme --- .../builtin_action_types/servicenow/schema.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts index 100b549958632..65edd569ed44b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts @@ -8,6 +8,26 @@ import { schema } from '@kbn/config-schema'; export const ConfigSchemaProps = { apiUrl: schema.string(), + casesConfiguration: schema.maybe( + schema.object({ + closure: schema.oneOf([ + schema.literal('manual'), + schema.literal('new_incident'), + schema.literal('closed_incident'), + ]), + mapping: schema.recordOf( + schema.string(), + schema.object({ + thirdPartyField: schema.string(), + onEditAndUpdate: schema.oneOf([ + schema.literal('nothing'), + schema.literal('overwrite'), + schema.literal('append'), + ]), + }) + ), + }) + ), }; export const ConfigSchema = schema.object(ConfigSchemaProps); From 82fae421f6bf61850cdae796e8b4c13125d514fc Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 4 Mar 2020 20:38:27 +0200 Subject: [PATCH 05/42] Refactor configuration schema --- .../builtin_action_types/servicenow/schema.ts | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts index 65edd569ed44b..b7ec3a4127e96 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts @@ -6,28 +6,28 @@ import { schema } from '@kbn/config-schema'; +export const MapsSchema = schema.object({ + source: schema.string(), + target: schema.string(), + onEditAndUpdate: schema.oneOf([ + schema.literal('nothing'), + schema.literal('overwrite'), + schema.literal('append'), + ]), +}); + +export const CasesConfigurationSchema = schema.object({ + closure: schema.oneOf([ + schema.literal('manual'), + schema.literal('new_incident'), + schema.literal('closed_incident'), + ]), + mapping: schema.arrayOf(MapsSchema), +}); + export const ConfigSchemaProps = { apiUrl: schema.string(), - casesConfiguration: schema.maybe( - schema.object({ - closure: schema.oneOf([ - schema.literal('manual'), - schema.literal('new_incident'), - schema.literal('closed_incident'), - ]), - mapping: schema.recordOf( - schema.string(), - schema.object({ - thirdPartyField: schema.string(), - onEditAndUpdate: schema.oneOf([ - schema.literal('nothing'), - schema.literal('overwrite'), - schema.literal('append'), - ]), - }) - ), - }) - ), + casesConfiguration: CasesConfigurationSchema, }; export const ConfigSchema = schema.object(ConfigSchemaProps); From 8ef660a77d78fa1649cf0ff0b6ad31b830e44aa7 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 4 Mar 2020 20:39:01 +0200 Subject: [PATCH 06/42] Refactor parameters schema --- .../server/builtin_action_types/servicenow/schema.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts index b7ec3a4127e96..db93b440751ab 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts @@ -39,7 +39,15 @@ export const SecretsSchemaProps = { export const SecretsSchema = schema.object(SecretsSchemaProps); +export const CommentSchema = schema.object({ + id: schema.string(), + comment: schema.string(), + version: schema.maybe(schema.string()), +}); + export const ParamsSchema = schema.object({ - comments: schema.maybe(schema.string()), - short_description: schema.string(), + id: schema.nullable(schema.string()), + comments: schema.maybe(schema.arrayOf(CommentSchema)), + description: schema.maybe(schema.string()), + title: schema.string(), }); From 4e710ad47b7636f9a6e95f82d3df5329a8a79256 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 4 Mar 2020 20:39:31 +0200 Subject: [PATCH 07/42] Create new types --- .../builtin_action_types/servicenow/types.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts index ee37c8d8eac39..00af7b32b64ec 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -6,7 +6,14 @@ import { TypeOf } from '@kbn/config-schema'; -import { ConfigSchema, SecretsSchema, ParamsSchema } from './schema'; +import { + ConfigSchema, + SecretsSchema, + ParamsSchema, + CasesConfigurationSchema, + MapsSchema, + CommentSchema, +} from './schema'; // config definition export type ConfigType = TypeOf; @@ -14,4 +21,12 @@ export type ConfigType = TypeOf; // secrets definition export type SecretsType = TypeOf; -export type ParamsType = TypeOf; +export type ParamsType = TypeOf & { + [key: string]: any; +}; + +export type CasesConfigurationType = TypeOf; +export type MapsType = TypeOf; +export type CommentType = TypeOf; + +export type FinalMapping = Map; From 91a85a6527923b923adf27de09660ac19573d989 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 4 Mar 2020 20:40:01 +0200 Subject: [PATCH 08/42] Add supported source fields --- .../actions/server/builtin_action_types/servicenow/constants.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/constants.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/constants.ts index 32d3f29edfdf0..a0ffd859e14ca 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/constants.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/constants.ts @@ -5,3 +5,4 @@ */ export const ACTION_TYPE_ID = '.servicenow'; +export const SUPPORTED_SOURCE_FIELDS = ['title', 'comments', 'description']; From 5af92be681269f6a44d5cb6d08e5f7d2a48d622b Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 4 Mar 2020 20:40:19 +0200 Subject: [PATCH 09/42] Create helpers --- .../servicenow/helpers.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts new file mode 100644 index 0000000000000..8cd7bb6d27c54 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts @@ -0,0 +1,43 @@ +/* + * 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 { SUPPORTED_SOURCE_FIELDS } from './constants'; +import { MapsType, FinalMapping, ParamsType } from './types'; + +export const sanitizeMapping = (fields: string[], mapping: MapsType[]): MapsType[] => { + // Prevent prototype pollution and remove unsupported fields + return mapping.filter( + m => m.source !== '__proto__' && m.target !== '__proto__' && fields.includes(m.source) + ); +}; + +export const buildMap = (mapping: MapsType[]): FinalMapping => { + // Maybe redundant as Map is safe against prototype pollution + const sanitizedMap = sanitizeMapping(SUPPORTED_SOURCE_FIELDS, mapping); + const fieldsMap = new Map(); + + for (const field of sanitizedMap) { + const { source, target, onEditAndUpdate } = field; + fieldsMap.set(source, { target, onEditAndUpdate }); + fieldsMap.set(target, { target: source, onEditAndUpdate }); + } + + return fieldsMap; +}; + +interface KeyAny { + [key: string]: any; +} + +export const mapParams = (params: any, mapping: FinalMapping) => { + return Object.keys(params).reduce((prev: KeyAny, curr: string): KeyAny => { + const field = mapping.get(curr); + if (field) { + prev[field.target] = params[curr]; + } + return prev; + }, {} as KeyAny); +}; From 9585d6bb23dd4614d6f7d49bb259ac7f157bafe0 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 4 Mar 2020 20:41:08 +0200 Subject: [PATCH 10/42] Create ServiceNow lib --- .../lib/servicenow/index.ts | 71 ++++++++++++++++++- .../lib/servicenow/types.ts | 13 ++++ 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.ts index 308921c8fd50b..32b4e6676492d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.ts @@ -4,11 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Instance } from './types'; +import axios, { AxiosInstance, Method, AxiosResponse } from 'axios'; + +import { Instance, Incident, IncidentResponse } from './types'; +import { CommentType } from '../../servicenow/types'; + +const API_VERSION = 'v1'; +const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`; +const USER_URL = `api/now/${API_VERSION}/table/sys_user?user_name=`; + +const validStatusCodes = [200, 201]; class ServiceNow { - private static readonly API_VERSION = 'v1'; - private static readonly INCIDENT_URL = `/api/now/${ServiceNow.API_VERSION}/table/incident`; + private readonly incidentUrl: string; + private readonly userUrl: string; + private readonly axios: AxiosInstance; constructor(private readonly instance: Instance) { if ( @@ -19,6 +29,61 @@ class ServiceNow { ) { throw Error('[Action][ServiceNow]: Wrong configuration.'); } + + this.incidentUrl = `${this.instance.url}/${INCIDENT_URL}`; + this.userUrl = `${this.instance.url}/${USER_URL}`; + this.axios = axios.create({ + auth: { username: this.instance.username, password: this.instance.password }, + }); + } + + _throwIfNotAlive(status: number, contentType: string) { + if (!validStatusCodes.includes(status) || !contentType.includes('application/json')) { + throw new Error('[ServiceNow]: Instance is not alive.'); + } + } + + async _request({ + url, + method = 'get', + data = {}, + }: { + url: string; + method?: Method; + data?: any; + }): Promise { + const res = await this.axios(url, { method, data }); + this._throwIfNotAlive(res.status, res.headers['content-type']); + return res; + } + + async getUserID(): Promise { + const res = await this._request({ url: `${this.userUrl}${this.instance.username}` }); + return res.data.result[0].sys_id; + } + + async createIncident(incident: Incident): Promise { + const res = await this._request({ + url: `${this.incidentUrl}`, + method: 'post', + data: { ...incident }, + }); + + return { number: res.data.result.number, id: res.data.result.sys_id }; + } + + async batchAddComments(incidentId: string, comments: string[], field: string): Promise { + for (const comment of comments) { + await this.addComment(incidentId, comment, field); + } + } + + async addComment(incidentId: string, comment: string, field: string): Promise { + await this._request({ + url: `${this.incidentUrl}/${incidentId}`, + method: 'patch', + data: { [field]: comment }, + }); } } diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/types.ts index ab4d0af1b5e96..23edf235cf5fb 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/types.ts @@ -4,8 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ +import { CommentType } from '../../servicenow/types'; + export interface Instance { url: string; username: string; password: string; } + +export interface Incident { + short_description: string; + description?: string; + caller_id?: string; +} + +export interface IncidentResponse { + number: string; + id: string; +} From 709e1e50962e777dd99d8cb5de7cea8042971ea2 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 4 Mar 2020 20:41:38 +0200 Subject: [PATCH 11/42] Push incident --- .../builtin_action_types/servicenow/index.ts | 68 ++++++++----------- 1 file changed, 29 insertions(+), 39 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index 926ba54c01f52..131c72acd2075 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -5,7 +5,7 @@ */ import { curry } from 'lodash'; -import { schema, TypeOf } from '@kbn/config-schema'; +import { schema } from '@kbn/config-schema'; import { ActionType, ActionTypeExecutorOptions, @@ -13,7 +13,7 @@ import { ExecutorType, } from '../../types'; import { ActionsConfigurationUtilities } from '../../actions_config'; -import { postServiceNow } from '../lib/post_servicenow'; +import { ServiceNow } from '../lib/servicenow'; import * as i18n from './translations'; @@ -22,6 +22,9 @@ import { ConfigType, SecretsType, ParamsType } from './types'; import { ConfigSchemaProps, SecretsSchemaProps, ParamsSchema } from './schema'; +import { buildMap, mapParams } from './helpers'; +import { Incident } from '../lib/servicenow/types'; + function validateConfig( configurationUtilities: ActionsConfigurationUtilities, configObject: ConfigType @@ -78,49 +81,36 @@ async function serviceNowExecutor( execOptions: ActionTypeExecutorOptions ): Promise { const actionId = execOptions.actionId; - const config = execOptions.config as ConfigType; - const secrets = execOptions.secrets as SecretsType; + const { + apiUrl, + casesConfiguration: { closure, mapping }, + } = execOptions.config as ConfigType; + const { username, password } = execOptions.secrets as SecretsType; const params = execOptions.params as ParamsType; - const headers = { - Accept: 'application/json', - 'Content-Type': 'application/json', - }; - let response; - try { - response = await postServiceNow({ apiUrl: config.apiUrl, data: params, headers, secrets }); - } catch (err) { - const message = i18n.ERROR_POSTING; - return { - status: 'error', - actionId, - message, - serviceMessage: err.message, - }; - } - if (response.status === 200 || response.status === 201 || response.status === 204) { - return { - status: 'ok', - actionId, - data: response.data, - }; - } + const { comments, ...restParams } = params; - if (response.status === 429 || response.status >= 500) { - const message = i18n.RETRY_POSTING(response.status); + const finalMap = buildMap(mapping); + const restMapped = mapParams(restParams, finalMap); + const paramsAsIncident = restMapped as Incident; - return { - status: 'error', - actionId, - message, - retry: true, - }; - } + const serviceNow = new ServiceNow({ url: apiUrl, username, password }); + const userId = await serviceNow.getUserID(); + const { id, number } = await serviceNow.createIncident({ + ...paramsAsIncident, + caller_id: userId, + }); - const message = i18n.UNEXPECTED_STATUS(response.status); + if (comments && Array.isArray(comments) && comments.length > 0) { + serviceNow.batchAddComments( + id, + comments.map(c => c.comment), + finalMap.get('comments').target + ); + } return { - status: 'error', + status: 'ok', actionId, - message, + data: { id, number }, }; } From 818f2feda008aeccaec77a80341d14e5e89b7d8b Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 4 Mar 2020 23:03:59 +0200 Subject: [PATCH 12/42] Declare private methods --- .../server/builtin_action_types/lib/servicenow/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.ts index 32b4e6676492d..88d02a049e429 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.ts @@ -37,13 +37,13 @@ class ServiceNow { }); } - _throwIfNotAlive(status: number, contentType: string) { + private _throwIfNotAlive(status: number, contentType: string) { if (!validStatusCodes.includes(status) || !contentType.includes('application/json')) { throw new Error('[ServiceNow]: Instance is not alive.'); } } - async _request({ + private async _request({ url, method = 'get', data = {}, From b537fc68f0d46a576a785d3eef32add1eb5c82f1 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 4 Mar 2020 23:04:35 +0200 Subject: [PATCH 13/42] Create UpdateIncident type --- .../server/builtin_action_types/lib/servicenow/types.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/types.ts index 23edf235cf5fb..7078794d7cd9e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/types.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CommentType } from '../../servicenow/types'; - export interface Instance { url: string; username: string; @@ -13,7 +11,7 @@ export interface Instance { } export interface Incident { - short_description: string; + short_description?: string; description?: string; caller_id?: string; } @@ -22,3 +20,5 @@ export interface IncidentResponse { number: string; id: string; } + +export type UpdateIncident = Partial; From 2f805bb5fc6d43405cd33ddb42929b374dbda41e Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 4 Mar 2020 23:05:46 +0200 Subject: [PATCH 14/42] Create updateIncident method --- .../lib/servicenow/index.ts | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.ts index 88d02a049e429..cdbc37d428812 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.ts @@ -6,8 +6,7 @@ import axios, { AxiosInstance, Method, AxiosResponse } from 'axios'; -import { Instance, Incident, IncidentResponse } from './types'; -import { CommentType } from '../../servicenow/types'; +import { Instance, Incident, IncidentResponse, UpdateIncident } from './types'; const API_VERSION = 'v1'; const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`; @@ -57,6 +56,14 @@ class ServiceNow { return res; } + private _patch({ url, data }: { url: string; data: any }): Promise { + return this._request({ + url, + method: 'patch', + data, + }); + } + async getUserID(): Promise { const res = await this._request({ url: `${this.userUrl}${this.instance.username}` }); return res.data.result[0].sys_id; @@ -72,6 +79,13 @@ class ServiceNow { return { number: res.data.result.number, id: res.data.result.sys_id }; } + async updateIncident(incidentId: string, incident: UpdateIncident): Promise { + await this._patch({ + url: `${this.incidentUrl}/${incidentId}`, + data: { ...incident }, + }); + } + async batchAddComments(incidentId: string, comments: string[], field: string): Promise { for (const comment of comments) { await this.addComment(incidentId, comment, field); @@ -79,9 +93,8 @@ class ServiceNow { } async addComment(incidentId: string, comment: string, field: string): Promise { - await this._request({ + await this._patch({ url: `${this.incidentUrl}/${incidentId}`, - method: 'patch', data: { [field]: comment }, }); } From cacef33566f0a5f2738f390c47257b1da408d6a0 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 4 Mar 2020 23:06:30 +0200 Subject: [PATCH 15/42] Create executor actions --- .../servicenow/action_handlers.ts | 53 ++++++++++++++++ .../builtin_action_types/servicenow/index.ts | 60 ++++++++++++------- .../builtin_action_types/servicenow/schema.ts | 9 ++- .../builtin_action_types/servicenow/types.ts | 14 +++++ 4 files changed, 113 insertions(+), 23 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts new file mode 100644 index 0000000000000..126434fb68239 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.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 { Incident } from '../lib/servicenow/types'; +import { ActionHandlerArguments, UpdateParamsType, UpdateActionHandlerArguments } from './types'; + +export const handleCreateIncident = async ({ + serviceNow, + params, + comments, + mapping, +}: ActionHandlerArguments) => { + const paramsAsIncident = params as Incident; + + const userId = await serviceNow.getUserID(); + const { id, number } = await serviceNow.createIncident({ + ...paramsAsIncident, + caller_id: userId, + }); + + if (comments && Array.isArray(comments) && comments.length > 0) { + await serviceNow.batchAddComments( + id, + comments.map(c => c.comment), + mapping.get('comments').target + ); + } + + return { id, number }; +}; + +export const handleUpdateIncident = async ({ + incidentId, + serviceNow, + params, + comments, + mapping, +}: UpdateActionHandlerArguments) => { + const paramsAsIncident = params as UpdateParamsType; + + await serviceNow.updateIncident(incidentId, { ...paramsAsIncident }); + + if (comments && Array.isArray(comments) && comments.length > 0) { + await serviceNow.batchAddComments( + incidentId, + comments.map(c => c.comment), + mapping.get('comments').target + ); + } +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index 131c72acd2075..de783c3d64d9d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -18,12 +18,12 @@ import { ServiceNow } from '../lib/servicenow'; import * as i18n from './translations'; import { ACTION_TYPE_ID } from './constants'; -import { ConfigType, SecretsType, ParamsType } from './types'; +import { ConfigType, SecretsType, ParamsType, CommentType } from './types'; import { ConfigSchemaProps, SecretsSchemaProps, ParamsSchema } from './schema'; import { buildMap, mapParams } from './helpers'; -import { Incident } from '../lib/servicenow/types'; +import { handleCreateIncident, handleUpdateIncident } from './action_handlers'; function validateConfig( configurationUtilities: ActionsConfigurationUtilities, @@ -87,30 +87,46 @@ async function serviceNowExecutor( } = execOptions.config as ConfigType; const { username, password } = execOptions.secrets as SecretsType; const params = execOptions.params as ParamsType; - const { comments, ...restParams } = params; + const { comments, executorAction, incidentId, ...restParams } = params; const finalMap = buildMap(mapping); const restMapped = mapParams(restParams, finalMap); - const paramsAsIncident = restMapped as Incident; - const serviceNow = new ServiceNow({ url: apiUrl, username, password }); - const userId = await serviceNow.getUserID(); - const { id, number } = await serviceNow.createIncident({ - ...paramsAsIncident, - caller_id: userId, - }); - - if (comments && Array.isArray(comments) && comments.length > 0) { - serviceNow.batchAddComments( - id, - comments.map(c => c.comment), - finalMap.get('comments').target - ); - } - return { - status: 'ok', - actionId, - data: { id, number }, + const handlerInput = { + serviceNow, + params: restMapped, + comments: comments as CommentType[], + mapping: finalMap, }; + + let res = {}; + + switch (executorAction) { + case 'newIncident': + res = await handleCreateIncident(handlerInput); + + return { + status: 'ok', + actionId, + data: { ...res }, + }; + + case 'updateIncident': + if (!incidentId) { + throw new Error('[Action][ServiceNow]: IncidentId is required.'); + } + + await handleUpdateIncident({ incidentId, ...handlerInput }); + return { + status: 'ok', + actionId, + }; + default: + return { + status: 'ok', + actionId, + serviceMessage: '[Action][ServiceNow]: Unimplemented executor action.', + }; + } } diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts index db93b440751ab..ae5ffb4fea952 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts @@ -45,9 +45,16 @@ export const CommentSchema = schema.object({ version: schema.maybe(schema.string()), }); +export const ExecutorAction = schema.oneOf([ + schema.literal('newIncident'), + schema.literal('updateIncident'), +]); + export const ParamsSchema = schema.object({ + executorAction: ExecutorAction, id: schema.nullable(schema.string()), comments: schema.maybe(schema.arrayOf(CommentSchema)), description: schema.maybe(schema.string()), - title: schema.string(), + title: schema.maybe(schema.string()), + incidentId: schema.maybe(schema.string()), }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts index 00af7b32b64ec..c378d7a47f177 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -15,6 +15,8 @@ import { CommentSchema, } from './schema'; +import { ServiceNow } from '../lib/servicenow'; + // config definition export type ConfigType = TypeOf; @@ -30,3 +32,15 @@ export type MapsType = TypeOf; export type CommentType = TypeOf; export type FinalMapping = Map; + +export interface ActionHandlerArguments { + serviceNow: ServiceNow; + params: any; + comments: CommentType[]; + mapping: FinalMapping; +} + +export type UpdateParamsType = Partial; +export type UpdateActionHandlerArguments = ActionHandlerArguments & { + incidentId: string; +}; From 389ae3d44cfb186049f03a85b6ecf3e63174593d Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 4 Mar 2020 23:31:29 +0200 Subject: [PATCH 16/42] Refactor response --- .../builtin_action_types/servicenow/index.ts | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index de783c3d64d9d..46e737e5f3bb3 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -100,16 +100,21 @@ async function serviceNowExecutor( mapping: finalMap, }; - let res = {}; + const res: Pick & + Pick = { + status: 'ok', + actionId, + }; + + let data = {}; switch (executorAction) { case 'newIncident': - res = await handleCreateIncident(handlerInput); + data = await handleCreateIncident(handlerInput); return { - status: 'ok', - actionId, - data: { ...res }, + ...res, + data, }; case 'updateIncident': @@ -119,13 +124,11 @@ async function serviceNowExecutor( await handleUpdateIncident({ incidentId, ...handlerInput }); return { - status: 'ok', - actionId, + ...res, }; default: return { - status: 'ok', - actionId, + ...res, serviceMessage: '[Action][ServiceNow]: Unimplemented executor action.', }; } From bf87ebeb286e7abe463613428b4f00a444e351f3 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 5 Mar 2020 12:11:13 +0200 Subject: [PATCH 17/42] Test helpers --- .../servicenow/helpers.test.ts | 55 +++++++++++++++++ .../builtin_action_types/servicenow/mock.ts | 59 +++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts new file mode 100644 index 0000000000000..efc6c3a1a881d --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.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 { sanitizeMapping, buildMap, mapParams } from './helpers'; +import { mapping, maliciousMapping, finalMapping, params } from './mock'; +import { SUPPORTED_SOURCE_FIELDS } from './constants'; + +describe('sanitizeMapping', () => { + test('remove malicious fields', () => { + const sanitizedMapping = sanitizeMapping(SUPPORTED_SOURCE_FIELDS, maliciousMapping); + expect(sanitizedMapping.every(m => m.source !== '__proto__' && m.target !== '__proto__')).toBe( + true + ); + }); + + test('remove unsuppported source fields', () => { + const sanitizedMapping = sanitizeMapping(SUPPORTED_SOURCE_FIELDS, maliciousMapping); + expect(sanitizedMapping).not.toEqual( + expect.arrayContaining([expect.objectContaining(maliciousMapping[3])]) + ); + }); +}); + +describe('buildMap', () => { + test('builds sanitized Map', () => { + const finalMap = buildMap(maliciousMapping); + expect(finalMap.get('__proto__')).not.toBeDefined(); + }); + + test('builds Map correct', () => { + const final = buildMap(mapping); + expect(final).toEqual(finalMapping); + }); +}); + +describe('mapParams', () => { + test('maps params correctly', () => { + const { comments, ...restParams } = params; + const fields = mapParams(restParams, finalMapping); + expect(fields).toEqual({ + [finalMapping.get('title').target]: restParams.title, + [finalMapping.get('description').target]: restParams.description, + }); + }); + + test('do not add fields not in mapping', () => { + const { comments, ...restParams } = params; + const fields = mapParams(restParams, finalMapping); + const { title, description, ...unexpectedFields } = restParams; + expect(fields).not.toEqual(expect.objectContaining(unexpectedFields)); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts new file mode 100644 index 0000000000000..37c0aba46eb14 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts @@ -0,0 +1,59 @@ +/* + * 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 { MapsType, FinalMapping, ParamsType } from './types'; + +const mapping: MapsType[] = [ + { source: 'title', target: 'short_description', onEditAndUpdate: 'nothing' }, + { source: 'description', target: 'description', onEditAndUpdate: 'nothing' }, + { source: 'comments', target: 'comments', onEditAndUpdate: 'nothing' }, +]; + +const maliciousMapping: MapsType[] = [ + { source: '__proto__', target: 'short_description', onEditAndUpdate: 'nothing' }, + { source: 'description', target: '__proto__', onEditAndUpdate: 'nothing' }, + { source: 'comments', target: 'comments', onEditAndUpdate: 'nothing' }, + { source: 'unsupportedSource', target: 'comments', onEditAndUpdate: 'nothing' }, +]; + +const finalMapping: FinalMapping = new Map(); + +finalMapping.set(mapping[0].source, { + target: mapping[0].target, + onEditAndUpdate: mapping[0].onEditAndUpdate, +}); + +finalMapping.set(mapping[1].source, { + target: mapping[1].target, + onEditAndUpdate: mapping[1].onEditAndUpdate, +}); + +finalMapping.set(mapping[2].source, { + target: mapping[2].target, + onEditAndUpdate: mapping[2].onEditAndUpdate, +}); + +finalMapping.set(mapping[0].target, { + target: mapping[0].source, + onEditAndUpdate: mapping[0].onEditAndUpdate, +}); + +const params: ParamsType = { + executorAction: 'updateIncident', + id: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', + incidentId: 'ceb5986e079f00100e48fbbf7c1ed06d', + title: 'Incident title', + description: 'Incident description', + comments: [ + { + id: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'A comment', + }, + ], +}; + +export { mapping, maliciousMapping, finalMapping, params }; From 6e75facd41bddbe36d9481b7871de8e8660b7363 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 5 Mar 2020 13:53:41 +0200 Subject: [PATCH 18/42] Remove unnecessary validation --- .../actions/server/builtin_action_types/servicenow/index.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index 46e737e5f3bb3..4f0000183affc 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -29,9 +29,6 @@ function validateConfig( configurationUtilities: ActionsConfigurationUtilities, configObject: ConfigType ) { - if (configObject.apiUrl == null) { - return i18n.API_URL_REQUIRED; - } try { configurationUtilities.ensureWhitelistedUri(configObject.apiUrl); } catch (whitelistError) { From 66ce0609e65b36ca99095a1b8e8596fa79e0b371 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 5 Mar 2020 13:54:59 +0200 Subject: [PATCH 19/42] Fix validation errors --- .../servicenow/index.test.ts | 76 ++++++++++++++----- 1 file changed, 55 insertions(+), 21 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts index 71a1141448523..64ee9a6711a18 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('./lib/post_servicenow', () => ({ +jest.mock('../lib/post_servicenow', () => ({ postServiceNow: jest.fn(), })); @@ -16,9 +16,13 @@ import { postServiceNow } from '../lib/post_servicenow'; import { createActionTypeRegistry } from '../index.test'; import { configUtilsMock } from '../../actions_config.mock'; -const postServiceNowMock = postServiceNow as jest.Mock; +import { ServiceNow } from '../lib/servicenow'; +import { ACTION_TYPE_ID } from './constants'; +import * as i18n from './translations'; + +jest.mock('../lib/servicenow'); -const ACTION_TYPE_ID = '.servicenow'; +const postServiceNowMock = postServiceNow as jest.Mock; const services: Services = { callCluster: async (path: string, opts: any) => {}, @@ -27,17 +31,49 @@ const services: Services = { let actionType: ActionType; -const mockServiceNow = { - config: { - apiUrl: 'www.servicenowisinkibanaactions.com', - }, +const mockOptions = { + name: 'servicenow-connector', + actionTypeId: '.servicenow', secrets: { - password: 'secret-password', username: 'secret-username', + password: 'secret-password', + }, + config: { + apiUrl: 'https://service-now.com', + casesConfiguration: { + closure: 'manual', + mapping: [ + { + source: 'title', + target: 'short_description', + onEditAndUpdate: 'overwrite', + }, + { + source: 'description', + target: 'description', + onEditAndUpdate: 'overwrite', + }, + { + source: 'comments', + target: 'work_notes', + onEditAndUpdate: 'append', + }, + ], + }, }, params: { - comments: 'hello cool service now incident', - short_description: 'this is a cool service now incident', + executorAction: 'updateIncident', + id: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', + incidentId: 'ceb5986e079f00100e48fbbf7c1ed06d', + title: 'Incident title', + description: 'Incident description', + comments: [ + { + id: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'A comment', + }, + ], }, }; @@ -49,13 +85,13 @@ beforeAll(() => { describe('get()', () => { test('should return correct action type', () => { expect(actionType.id).toEqual(ACTION_TYPE_ID); - expect(actionType.name).toEqual('ServiceNow'); + expect(actionType.name).toEqual(i18n.NAME); }); }); describe('validateConfig()', () => { test('should validate and pass when config is valid', () => { - const { config } = mockServiceNow; + const { config } = mockOptions; expect(validateConfig(actionType, config)).toEqual(config); }); @@ -72,14 +108,12 @@ describe('validateConfig()', () => { configurationUtilities: { ...configUtilsMock, ensureWhitelistedUri: url => { - expect(url).toEqual('https://events.servicenow.com/v2/enqueue'); + expect(url).toEqual(mockOptions.config.apiUrl); }, }, }); - expect( - validateConfig(actionType, { apiUrl: 'https://events.servicenow.com/v2/enqueue' }) - ).toEqual({ apiUrl: 'https://events.servicenow.com/v2/enqueue' }); + expect(validateConfig(actionType, mockOptions.config)).toEqual(mockOptions.config); }); test('config validation returns an error if the specified URL isnt whitelisted', () => { @@ -93,7 +127,7 @@ describe('validateConfig()', () => { }); expect(() => { - validateConfig(actionType, { apiUrl: 'https://events.servicenow.com/v2/enqueue' }); + validateConfig(actionType, mockOptions.config); }).toThrowErrorMatchingInlineSnapshot( `"error validating action type config: error configuring servicenow action: target url is not whitelisted"` ); @@ -102,7 +136,7 @@ describe('validateConfig()', () => { describe('validateSecrets()', () => { test('should validate and pass when secrets is valid', () => { - const { secrets } = mockServiceNow; + const { secrets } = mockOptions; expect(validateSecrets(actionType, secrets)).toEqual(secrets); }); @@ -123,15 +157,15 @@ describe('validateSecrets()', () => { describe('validateParams()', () => { test('should validate and pass when params is valid', () => { - const { params } = mockServiceNow; + const { params } = mockOptions; expect(validateParams(actionType, params)).toEqual(params); }); test('should validate and throw error when params is invalid', () => { expect(() => { - validateParams(actionType, { eventAction: 'ackynollage' }); + validateParams(actionType, {}); }).toThrowErrorMatchingInlineSnapshot( - `"error validating action params: [short_description]: expected value of type [string] but got [undefined]"` + `"error validating action params: [executorAction]: expected at least one defined value but got [undefined]"` ); }); }); From 33f3d6e32851f3e1faf98bd2be8a7364763c8106 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 5 Mar 2020 15:07:14 +0200 Subject: [PATCH 20/42] Throw error for unsupported actions --- .../actions/server/builtin_action_types/servicenow/index.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index 4f0000183affc..24252c955b7d2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -124,9 +124,6 @@ async function serviceNowExecutor( ...res, }; default: - return { - ...res, - serviceMessage: '[Action][ServiceNow]: Unimplemented executor action.', - }; + throw new Error('[Action][ServiceNow]: Unsupported executor action.'); } } From 700d82cba09e343dc6d38bc767b007bdf7794d09 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 5 Mar 2020 15:15:51 +0200 Subject: [PATCH 21/42] Create mock incident --- .../actions/server/builtin_action_types/servicenow/mock.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts index 37c0aba46eb14..ce5305107fbdf 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts @@ -56,4 +56,9 @@ const params: ParamsType = { ], }; -export { mapping, maliciousMapping, finalMapping, params }; +const responseIncident = { + id: 'c816f79cc0a8016401c5a33be04be441', + number: 'INC0010001', +}; + +export { mapping, maliciousMapping, finalMapping, params, responseIncident }; From b5d95f792b72e2907cd123f1cbef5af85b9ed6e7 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 5 Mar 2020 15:16:26 +0200 Subject: [PATCH 22/42] Test executor --- .../servicenow/index.test.ts | 204 +++++++++--------- 1 file changed, 96 insertions(+), 108 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts index 64ee9a6711a18..01a35cabef601 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts @@ -4,25 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('../lib/post_servicenow', () => ({ - postServiceNow: jest.fn(), -})); - import { getActionType } from '.'; import { ActionType, Services, ActionTypeExecutorOptions } from '../../types'; import { validateConfig, validateSecrets, validateParams } from '../../lib'; import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; -import { postServiceNow } from '../lib/post_servicenow'; import { createActionTypeRegistry } from '../index.test'; import { configUtilsMock } from '../../actions_config.mock'; -import { ServiceNow } from '../lib/servicenow'; import { ACTION_TYPE_ID } from './constants'; import * as i18n from './translations'; -jest.mock('../lib/servicenow'); +import { handleCreateIncident, handleUpdateIncident } from './action_handlers'; +import { responseIncident } from './mock'; + +jest.mock('./action_handlers'); -const postServiceNowMock = postServiceNow as jest.Mock; +const handleCreateIncidentMock = handleCreateIncident as jest.Mock; +const handleUpdateIncidentMock = handleUpdateIncident as jest.Mock; const services: Services = { callCluster: async (path: string, opts: any) => {}, @@ -172,142 +170,132 @@ describe('validateParams()', () => { describe('execute()', () => { beforeEach(() => { - postServiceNowMock.mockReset(); + handleCreateIncidentMock.mockReset(); + handleUpdateIncidentMock.mockReset(); }); - const { config, params, secrets } = mockServiceNow; - test('should succeed with valid params', async () => { - postServiceNowMock.mockImplementation(() => { - return { status: 201, data: 'data-here' }; - }); - const actionId = 'some-action-id'; + test('should create an incident', async () => { + const actionId = 'some-id'; const executorOptions: ActionTypeExecutorOptions = { actionId, - config, - params, - secrets, + config: mockOptions.config, + params: { ...mockOptions.params, executorAction: 'newIncident' }, + secrets: mockOptions.secrets, services, }; + + handleCreateIncidentMock.mockImplementation(() => responseIncident); + const actionResponse = await actionType.executor(executorOptions); - const { apiUrl, data, headers } = postServiceNowMock.mock.calls[0][0]; - expect({ apiUrl, data, headers, secrets }).toMatchInlineSnapshot(` - Object { - "apiUrl": "www.servicenowisinkibanaactions.com", - "data": Object { - "comments": "hello cool service now incident", - "short_description": "this is a cool service now incident", - }, - "headers": Object { - "Accept": "application/json", - "Content-Type": "application/json", - }, - "secrets": Object { - "password": "secret-password", - "username": "secret-username", - }, - } - `); - expect(actionResponse).toMatchInlineSnapshot(` - Object { - "actionId": "some-action-id", - "data": "data-here", - "status": "ok", - } - `); + expect(actionResponse).toEqual({ actionId, status: 'ok', data: responseIncident }); }); - test('should fail when postServiceNow throws', async () => { - postServiceNowMock.mockImplementation(() => { - throw new Error('doing some testing'); - }); + test('should throw an error when failed to create incident', async () => { + expect.assertions(1); - const actionId = 'some-action-id'; + const actionId = 'some-id'; const executorOptions: ActionTypeExecutorOptions = { actionId, - config, - params, - secrets, + config: mockOptions.config, + params: { ...mockOptions.params, executorAction: 'newIncident' }, + secrets: mockOptions.secrets, services, }; - const actionResponse = await actionType.executor(executorOptions); - expect(actionResponse).toMatchInlineSnapshot(` - Object { - "actionId": "some-action-id", - "message": "error posting servicenow event", - "serviceMessage": "doing some testing", - "status": "error", - } - `); - }); + const errorMessage = 'Failed to create incident'; - test('should fail when postServiceNow returns 429', async () => { - postServiceNowMock.mockImplementation(() => { - return { status: 429, data: 'data-here' }; + handleCreateIncidentMock.mockImplementation(() => { + throw new Error(errorMessage); }); - const actionId = 'some-action-id'; + try { + await actionType.executor(executorOptions); + } catch (error) { + expect(error.message).toEqual(errorMessage); + } + }); + + test('should update an incident', async () => { + const actionId = 'some-id'; const executorOptions: ActionTypeExecutorOptions = { actionId, - config, - params, - secrets, + config: mockOptions.config, + params: { ...mockOptions.params, executorAction: 'updateIncident' }, + secrets: mockOptions.secrets, services, }; + const actionResponse = await actionType.executor(executorOptions); - expect(actionResponse).toMatchInlineSnapshot(` - Object { - "actionId": "some-action-id", - "message": "error posting servicenow event: http status 429, retry later", - "retry": true, - "status": "error", - } - `); + expect(actionResponse).toEqual({ actionId, status: 'ok' }); }); - test('should fail when postServiceNow returns 501', async () => { - postServiceNowMock.mockImplementation(() => { - return { status: 501, data: 'data-here' }; - }); + test('should throw an error when failed to update an incident', async () => { + expect.assertions(1); - const actionId = 'some-action-id'; + const actionId = 'some-id'; const executorOptions: ActionTypeExecutorOptions = { actionId, - config, - params, - secrets, + config: mockOptions.config, + params: { ...mockOptions.params, executorAction: 'updateIncident' }, + secrets: mockOptions.secrets, services, }; - const actionResponse = await actionType.executor(executorOptions); - expect(actionResponse).toMatchInlineSnapshot(` - Object { - "actionId": "some-action-id", - "message": "error posting servicenow event: http status 501, retry later", - "retry": true, - "status": "error", - } - `); + const errorMessage = 'Failed to update incident'; + + handleUpdateIncidentMock.mockImplementation(() => { + throw new Error(errorMessage); + }); + + try { + await actionType.executor(executorOptions); + } catch (error) { + expect(error.message).toEqual(errorMessage); + } }); - test('should fail when postServiceNow returns 418', async () => { - postServiceNowMock.mockImplementation(() => { - return { status: 418, data: 'data-here' }; + test('should throw an error when incidentId is missing', async () => { + expect.assertions(1); + + const actionId = 'some-id'; + const { incidentId, ...rest } = mockOptions.params; + const executorOptions: ActionTypeExecutorOptions = { + actionId, + config: mockOptions.config, + params: { ...rest, executorAction: 'updateIncident' }, + secrets: mockOptions.secrets, + services, + }; + + const errorMessage = '[Action][ServiceNow]: IncidentId is required.'; + + handleUpdateIncidentMock.mockImplementation(() => { + throw new Error(errorMessage); }); - const actionId = 'some-action-id'; + try { + await actionType.executor(executorOptions); + } catch (error) { + expect(error.message).toEqual(errorMessage); + } + }); + + test('should throw an error for unsupported executor actions', async () => { + expect.assertions(1); + + const actionId = 'some-id'; const executorOptions: ActionTypeExecutorOptions = { actionId, - config, - params, - secrets, + config: mockOptions.config, + params: { ...mockOptions.params, executorAction: 'unsupported' }, + secrets: mockOptions.secrets, services, }; - const actionResponse = await actionType.executor(executorOptions); - expect(actionResponse).toMatchInlineSnapshot(` - Object { - "actionId": "some-action-id", - "message": "error posting servicenow event: unexpected status 418", - "status": "error", - } - `); + + const errorMessage = '[Action][ServiceNow]: Unsupported executor action.'; + + try { + await actionType.executor(executorOptions); + } catch (error) { + expect(error.message).toEqual(errorMessage); + } }); }); From 6998d8fd2e14253d5193be1ca6c98d19d55ea48c Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 5 Mar 2020 17:30:59 +0200 Subject: [PATCH 23/42] Test ServiceNow lib --- .../lib/servicenow/constants.ts | 9 ++ .../lib/servicenow/index.test.ts | 152 ++++++++++++++++++ .../lib/servicenow/index.ts | 5 +- .../servicenow/index.test.ts | 6 +- .../builtin_action_types/servicenow/mock.ts | 50 +++++- 5 files changed, 213 insertions(+), 9 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/constants.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.test.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/constants.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/constants.ts new file mode 100644 index 0000000000000..a2f13577dc2b2 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/constants.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const API_VERSION = 'v1'; +export const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`; +export const USER_URL = `api/now/${API_VERSION}/table/sys_user?user_name=`; diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.test.ts new file mode 100644 index 0000000000000..50a65407426aa --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.test.ts @@ -0,0 +1,152 @@ +/* + * 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 axios from 'axios'; +import { ServiceNow } from '.'; +import { + instance, + incident, + axiosResponse, + userIdResponse, + incidentAxiosResponse, + incidentResponse, + params, +} from '../../servicenow/mock'; +import { USER_URL, INCIDENT_URL } from './constants'; + +jest.mock('axios'); + +axios.create = jest.fn(() => axios); +const axiosMock = (axios as unknown) as jest.Mock; + +let serviceNow: ServiceNow; + +const testMissingConfiguration = (field: string) => { + expect.assertions(1); + try { + new ServiceNow({ ...instance, [field]: '' }); + } catch (error) { + expect(error.message).toEqual('[Action][ServiceNow]: Wrong configuration.'); + } +}; + +const prependInstanceUrl = (url: string): string => `${instance.url}/${url}`; + +describe('ServiceNow lib', () => { + beforeEach(() => { + serviceNow = new ServiceNow(instance); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should thrown an error if url is missing', () => { + testMissingConfiguration('url'); + }); + + test('should thrown an error if username is missing', () => { + testMissingConfiguration('username'); + }); + + test('should thrown an error if password is missing', () => { + testMissingConfiguration('password'); + }); + + test('get user id', async () => { + axiosMock.mockResolvedValue({ ...axiosResponse, data: userIdResponse }); + const res = await serviceNow.getUserID(); + const [url, { method }] = axiosMock.mock.calls[0]; + + expect(url).toEqual(prependInstanceUrl(`${USER_URL}${instance.username}`)); + expect(method).toEqual('get'); + expect(res).toEqual(userIdResponse.result[0].sys_id); + }); + + test('create incident', async () => { + axiosMock.mockResolvedValue({ ...axiosResponse, data: incidentAxiosResponse }); + const res = await serviceNow.createIncident(incident); + const [url, { method }] = axiosMock.mock.calls[0]; + + expect(url).toEqual(prependInstanceUrl(`${INCIDENT_URL}`)); + expect(method).toEqual('post'); + expect(res).toEqual(incidentResponse); + }); + + test('update incident', async () => { + axiosMock.mockResolvedValue({ ...axiosResponse, data: {} }); + const res = await serviceNow.updateIncident(params.incidentId!, { + short_description: params.title, + }); + const [url, { method }] = axiosMock.mock.calls[0]; + + expect(url).toEqual(prependInstanceUrl(`${INCIDENT_URL}/${params.incidentId}`)); + expect(method).toEqual('patch'); + expect(res).not.toBeDefined(); + }); + + test('add comment', async () => { + const fieldKey = 'comments'; + + axiosMock.mockResolvedValue({ ...axiosResponse, data: {} }); + const res = await serviceNow.addComment( + params.incidentId!, + params.comments![0].comment, + fieldKey + ); + + const [url, { method, data }] = axiosMock.mock.calls[0]; + + expect(url).toEqual(prependInstanceUrl(`${INCIDENT_URL}/${params.incidentId}`)); + expect(method).toEqual('patch'); + expect(data).toEqual({ [fieldKey]: params.comments![0].comment }); + expect(res).not.toBeDefined(); + }); + + test('add batch comment', async () => { + const fieldKey = 'comments'; + + axiosMock.mockResolvedValue({ ...axiosResponse, data: {} }); + const res = await serviceNow.batchAddComments( + params.incidentId!, + params.comments!.map(c => c.comment), + fieldKey + ); + + for (let i = 0; i < params.comments!.length; i++) { + const [url, { method, data }] = axiosMock.mock.calls[i]; + expect(url).toEqual(prependInstanceUrl(`${INCIDENT_URL}/${params.incidentId}`)); + expect(method).toEqual('patch'); + expect(data).toEqual({ [fieldKey]: params.comments![i].comment }); + expect(res).not.toBeDefined(); + } + }); + + test('throw if not status is not ok', async () => { + expect.assertions(1); + + axiosMock.mockResolvedValue({ ...axiosResponse, status: 401 }); + try { + await serviceNow.getUserID(); + } catch (error) { + expect(error.message).toEqual('[ServiceNow]: Instance is not alive.'); + } + }); + + test('throw if not content-type is not application/json', async () => { + expect.assertions(1); + + axiosMock.mockResolvedValue({ + ...axiosResponse, + headers: { 'content-type': 'application/html' }, + }); + try { + await serviceNow.getUserID(); + } catch (error) { + expect(error.message).toEqual('[ServiceNow]: Instance is not alive.'); + } + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.ts index cdbc37d428812..b022cdba1a33c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.ts @@ -6,12 +6,9 @@ import axios, { AxiosInstance, Method, AxiosResponse } from 'axios'; +import { INCIDENT_URL, USER_URL } from './constants'; import { Instance, Incident, IncidentResponse, UpdateIncident } from './types'; -const API_VERSION = 'v1'; -const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`; -const USER_URL = `api/now/${API_VERSION}/table/sys_user?user_name=`; - const validStatusCodes = [200, 201]; class ServiceNow { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts index 01a35cabef601..0c80b8377787b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts @@ -15,7 +15,7 @@ import { ACTION_TYPE_ID } from './constants'; import * as i18n from './translations'; import { handleCreateIncident, handleUpdateIncident } from './action_handlers'; -import { responseIncident } from './mock'; +import { incidentResponse } from './mock'; jest.mock('./action_handlers'); @@ -184,10 +184,10 @@ describe('execute()', () => { services, }; - handleCreateIncidentMock.mockImplementation(() => responseIncident); + handleCreateIncidentMock.mockImplementation(() => incidentResponse); const actionResponse = await actionType.executor(executorOptions); - expect(actionResponse).toEqual({ actionId, status: 'ok', data: responseIncident }); + expect(actionResponse).toEqual({ actionId, status: 'ok', data: incidentResponse }); }); test('should throw an error when failed to create incident', async () => { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts index ce5305107fbdf..f876f39dae44b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts @@ -5,6 +5,7 @@ */ import { MapsType, FinalMapping, ParamsType } from './types'; +import { Incident } from '../lib/servicenow/types'; const mapping: MapsType[] = [ { source: 'title', target: 'short_description', onEditAndUpdate: 'nothing' }, @@ -53,12 +54,57 @@ const params: ParamsType = { version: 'WzU3LDFd', comment: 'A comment', }, + { + id: 'e3db587f-ca27-4ae9-ad2e-31f2dcc9bd0d', + version: 'WlK3LDFd', + comment: 'Another comment', + }, ], }; -const responseIncident = { +const incidentResponse = { id: 'c816f79cc0a8016401c5a33be04be441', number: 'INC0010001', }; -export { mapping, maliciousMapping, finalMapping, params, responseIncident }; +const userId = '2e9a0a5e2f79001016ab51172799b670'; + +const axiosResponse = { + status: 200, + headers: { + 'content-type': 'application/json', + }, +}; +const userIdResponse = { + result: [{ sys_id: userId }], +}; + +const incidentAxiosResponse = { + result: { sys_id: incidentResponse.id, number: incidentResponse.number }, +}; + +const instance = { + url: 'https://instance.service-now.com', + username: 'username', + password: 'password', +}; + +const incident: Incident = { + short_description: params.title, + description: params.description, + caller_id: userId, +}; + +export { + mapping, + maliciousMapping, + finalMapping, + params, + incidentResponse, + incidentAxiosResponse, + userId, + userIdResponse, + axiosResponse, + instance, + incident, +}; From e3926507f760a3fc4fd9398b398f34ae2c3b0eaf Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 6 Mar 2020 09:44:32 +0200 Subject: [PATCH 24/42] Convert to camelCase --- .../actions/server/builtin_action_types/servicenow/schema.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts index ae5ffb4fea952..c4753ea756e50 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts @@ -19,8 +19,8 @@ export const MapsSchema = schema.object({ export const CasesConfigurationSchema = schema.object({ closure: schema.oneOf([ schema.literal('manual'), - schema.literal('new_incident'), - schema.literal('closed_incident'), + schema.literal('newIncident'), + schema.literal('closedIncident'), ]), mapping: schema.arrayOf(MapsSchema), }); From 9ff7184ee0c715f3518f43bc7d747e64eccf4ad3 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 6 Mar 2020 10:08:44 +0200 Subject: [PATCH 25/42] Remove caller_id --- .../server/builtin_action_types/servicenow/action_handlers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts index 126434fb68239..6b7cab5684b46 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts @@ -15,12 +15,11 @@ export const handleCreateIncident = async ({ }: ActionHandlerArguments) => { const paramsAsIncident = params as Incident; - const userId = await serviceNow.getUserID(); const { id, number } = await serviceNow.createIncident({ ...paramsAsIncident, - caller_id: userId, }); + // Should return comment ID if (comments && Array.isArray(comments) && comments.length > 0) { await serviceNow.batchAddComments( id, @@ -43,6 +42,7 @@ export const handleUpdateIncident = async ({ await serviceNow.updateIncident(incidentId, { ...paramsAsIncident }); + // Should return comment ID if (comments && Array.isArray(comments) && comments.length > 0) { await serviceNow.batchAddComments( incidentId, From d7afa1b1453afecc46bd21e2ad5ecd2d5f97d27b Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 6 Mar 2020 10:09:07 +0200 Subject: [PATCH 26/42] Refactor helpers --- .../servicenow/helpers.test.ts | 16 +++++++++++----- .../servicenow/helpers.ts | 19 +++++++------------ 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts index efc6c3a1a881d..c2f85c6790de6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts @@ -4,22 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import { sanitizeMapping, buildMap, mapParams } from './helpers'; +import { normalizeMapping, buildMap, mapParams } from './helpers'; import { mapping, maliciousMapping, finalMapping, params } from './mock'; import { SUPPORTED_SOURCE_FIELDS } from './constants'; describe('sanitizeMapping', () => { test('remove malicious fields', () => { - const sanitizedMapping = sanitizeMapping(SUPPORTED_SOURCE_FIELDS, maliciousMapping); + const sanitizedMapping = normalizeMapping(SUPPORTED_SOURCE_FIELDS, maliciousMapping); expect(sanitizedMapping.every(m => m.source !== '__proto__' && m.target !== '__proto__')).toBe( true ); }); test('remove unsuppported source fields', () => { - const sanitizedMapping = sanitizeMapping(SUPPORTED_SOURCE_FIELDS, maliciousMapping); - expect(sanitizedMapping).not.toEqual( - expect.arrayContaining([expect.objectContaining(maliciousMapping[3])]) + const normalizedMapping = normalizeMapping(SUPPORTED_SOURCE_FIELDS, maliciousMapping); + expect(normalizedMapping).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + source: 'unsupportedSource', + target: 'comments', + onEditAndUpdate: 'nothing', + }), + ]) ); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts index 8cd7bb6d27c54..605743856f668 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts @@ -5,9 +5,9 @@ */ import { SUPPORTED_SOURCE_FIELDS } from './constants'; -import { MapsType, FinalMapping, ParamsType } from './types'; +import { MapsType, FinalMapping } from './types'; -export const sanitizeMapping = (fields: string[], mapping: MapsType[]): MapsType[] => { +export const normalizeMapping = (fields: string[], mapping: MapsType[]): MapsType[] => { // Prevent prototype pollution and remove unsupported fields return mapping.filter( m => m.source !== '__proto__' && m.target !== '__proto__' && fields.includes(m.source) @@ -15,21 +15,16 @@ export const sanitizeMapping = (fields: string[], mapping: MapsType[]): MapsType }; export const buildMap = (mapping: MapsType[]): FinalMapping => { - // Maybe redundant as Map is safe against prototype pollution - const sanitizedMap = sanitizeMapping(SUPPORTED_SOURCE_FIELDS, mapping); - const fieldsMap = new Map(); - - for (const field of sanitizedMap) { + return normalizeMapping(SUPPORTED_SOURCE_FIELDS, mapping).reduce((fieldsMap, field) => { const { source, target, onEditAndUpdate } = field; fieldsMap.set(source, { target, onEditAndUpdate }); fieldsMap.set(target, { target: source, onEditAndUpdate }); - } - - return fieldsMap; + return fieldsMap; + }, new Map()); }; interface KeyAny { - [key: string]: any; + [key: string]: unknown; } export const mapParams = (params: any, mapping: FinalMapping) => { @@ -39,5 +34,5 @@ export const mapParams = (params: any, mapping: FinalMapping) => { prev[field.target] = params[curr]; } return prev; - }, {} as KeyAny); + }, {}); }; From d04498183abeadc00a3b0b6c01c76d6782214556 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 6 Mar 2020 10:09:53 +0200 Subject: [PATCH 27/42] Refactor schema --- .../server/builtin_action_types/servicenow/mock.ts | 10 ++++++---- .../server/builtin_action_types/servicenow/schema.ts | 6 +++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts index f876f39dae44b..2c2aa9dbb8d5a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts @@ -5,7 +5,7 @@ */ import { MapsType, FinalMapping, ParamsType } from './types'; -import { Incident } from '../lib/servicenow/types'; +import { Incident, IncidentResponse, UpdateIncident } from '../lib/servicenow/types'; const mapping: MapsType[] = [ { source: 'title', target: 'short_description', onEditAndUpdate: 'nothing' }, @@ -44,20 +44,22 @@ finalMapping.set(mapping[0].target, { const params: ParamsType = { executorAction: 'updateIncident', - id: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', + caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', incidentId: 'ceb5986e079f00100e48fbbf7c1ed06d', title: 'Incident title', description: 'Incident description', comments: [ { - id: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', version: 'WzU3LDFd', comment: 'A comment', + incidentCommentId: '263ede42075300100e48fbbf7c1ed047', }, { - id: 'e3db587f-ca27-4ae9-ad2e-31f2dcc9bd0d', + commentId: 'e3db587f-ca27-4ae9-ad2e-31f2dcc9bd0d', version: 'WlK3LDFd', comment: 'Another comment', + incidentCommentId: '315e1ece071300100e48fbbf7c1ed0d0', }, ], }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts index c4753ea756e50..eb939d815cc47 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts @@ -40,9 +40,10 @@ export const SecretsSchemaProps = { export const SecretsSchema = schema.object(SecretsSchemaProps); export const CommentSchema = schema.object({ - id: schema.string(), + commentId: schema.string(), comment: schema.string(), version: schema.maybe(schema.string()), + incidentCommentId: schema.maybe(schema.string()), }); export const ExecutorAction = schema.oneOf([ @@ -51,8 +52,7 @@ export const ExecutorAction = schema.oneOf([ ]); export const ParamsSchema = schema.object({ - executorAction: ExecutorAction, - id: schema.nullable(schema.string()), + caseId: schema.string(), comments: schema.maybe(schema.arrayOf(CommentSchema)), description: schema.maybe(schema.string()), title: schema.maybe(schema.string()), From 9c9b25ee7ed8bb0a6b2e29c22983da336c2db9c9 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 6 Mar 2020 10:10:12 +0200 Subject: [PATCH 28/42] Remove executorAction --- .../servicenow/index.test.ts | 62 +++---------------- .../builtin_action_types/servicenow/index.ts | 40 +++++------- 2 files changed, 25 insertions(+), 77 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts index 0c80b8377787b..8a29fa2731007 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts @@ -60,16 +60,16 @@ const mockOptions = { }, }, params: { - executorAction: 'updateIncident', - id: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', + caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', incidentId: 'ceb5986e079f00100e48fbbf7c1ed06d', title: 'Incident title', description: 'Incident description', comments: [ { - id: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', version: 'WzU3LDFd', comment: 'A comment', + incidentCommentId: '315e1ece071300100e48fbbf7c1ed0d0', }, ], }, @@ -163,7 +163,7 @@ describe('validateParams()', () => { expect(() => { validateParams(actionType, {}); }).toThrowErrorMatchingInlineSnapshot( - `"error validating action params: [executorAction]: expected at least one defined value but got [undefined]"` + `"error validating action params: [caseId]: expected value of type [string] but got [undefined]"` ); }); }); @@ -176,10 +176,12 @@ describe('execute()', () => { test('should create an incident', async () => { const actionId = 'some-id'; + const { incidentId, ...rest } = mockOptions.params; + const executorOptions: ActionTypeExecutorOptions = { actionId, config: mockOptions.config, - params: { ...mockOptions.params, executorAction: 'newIncident' }, + params: { ...rest }, secrets: mockOptions.secrets, services, }; @@ -192,12 +194,13 @@ describe('execute()', () => { test('should throw an error when failed to create incident', async () => { expect.assertions(1); + const { incidentId, ...rest } = mockOptions.params; const actionId = 'some-id'; const executorOptions: ActionTypeExecutorOptions = { actionId, config: mockOptions.config, - params: { ...mockOptions.params, executorAction: 'newIncident' }, + params: { ...rest }, secrets: mockOptions.secrets, services, }; @@ -251,51 +254,4 @@ describe('execute()', () => { expect(error.message).toEqual(errorMessage); } }); - - test('should throw an error when incidentId is missing', async () => { - expect.assertions(1); - - const actionId = 'some-id'; - const { incidentId, ...rest } = mockOptions.params; - const executorOptions: ActionTypeExecutorOptions = { - actionId, - config: mockOptions.config, - params: { ...rest, executorAction: 'updateIncident' }, - secrets: mockOptions.secrets, - services, - }; - - const errorMessage = '[Action][ServiceNow]: IncidentId is required.'; - - handleUpdateIncidentMock.mockImplementation(() => { - throw new Error(errorMessage); - }); - - try { - await actionType.executor(executorOptions); - } catch (error) { - expect(error.message).toEqual(errorMessage); - } - }); - - test('should throw an error for unsupported executor actions', async () => { - expect.assertions(1); - - const actionId = 'some-id'; - const executorOptions: ActionTypeExecutorOptions = { - actionId, - config: mockOptions.config, - params: { ...mockOptions.params, executorAction: 'unsupported' }, - secrets: mockOptions.secrets, - services, - }; - - const errorMessage = '[Action][ServiceNow]: Unsupported executor action.'; - - try { - await actionType.executor(executorOptions); - } catch (error) { - expect(error.message).toEqual(errorMessage); - } - }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index 24252c955b7d2..8a357d648d20e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -80,19 +80,19 @@ async function serviceNowExecutor( const actionId = execOptions.actionId; const { apiUrl, - casesConfiguration: { closure, mapping }, + casesConfiguration: { mapping }, } = execOptions.config as ConfigType; const { username, password } = execOptions.secrets as SecretsType; const params = execOptions.params as ParamsType; - const { comments, executorAction, incidentId, ...restParams } = params; + const { comments, incidentId, ...restParams } = params; const finalMap = buildMap(mapping); - const restMapped = mapParams(restParams, finalMap); + const restParamsMapped = mapParams(restParams, finalMap); const serviceNow = new ServiceNow({ url: apiUrl, username, password }); const handlerInput = { serviceNow, - params: restMapped, + params: restParamsMapped, comments: comments as CommentType[], mapping: finalMap, }; @@ -105,25 +105,17 @@ async function serviceNowExecutor( let data = {}; - switch (executorAction) { - case 'newIncident': - data = await handleCreateIncident(handlerInput); - - return { - ...res, - data, - }; - - case 'updateIncident': - if (!incidentId) { - throw new Error('[Action][ServiceNow]: IncidentId is required.'); - } - - await handleUpdateIncident({ incidentId, ...handlerInput }); - return { - ...res, - }; - default: - throw new Error('[Action][ServiceNow]: Unsupported executor action.'); + if (!incidentId) { + data = await handleCreateIncident(handlerInput); + + return { + ...res, + data, + }; + } else { + await handleUpdateIncident({ incidentId, ...handlerInput }); + return { + ...res, + }; } } From 4ad2fddb268edf909b433d05a17c4871c01270c8 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 6 Mar 2020 10:36:36 +0200 Subject: [PATCH 29/42] Test action handlers --- .../servicenow/action_handlers.test.ts | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts new file mode 100644 index 0000000000000..245c079b59eac --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts @@ -0,0 +1,126 @@ +/* + * 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 { handleCreateIncident, handleUpdateIncident } from './action_handlers'; +import { ServiceNow } from '../lib/servicenow'; +import { finalMapping } from './mock'; +import { Incident, UpdateIncident } from '../lib/servicenow/types'; + +jest.mock('../lib/servicenow'); + +const ServiceNowMock = ServiceNow as jest.Mock; + +const incident: Incident = { + short_description: 'A title', + description: 'A description', +}; + +const comments = [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'A comment', + incidentCommentId: '263ede42075300100e48fbbf7c1ed047', + }, + { + commentId: 'e3db587f-ca27-4ae9-ad2e-31f2dcc9bd0d', + version: 'WlK3LDFd', + comment: 'Another comment', + incidentCommentId: '315e1ece071300100e48fbbf7c1ed0d0', + }, +]; + +describe('handleCreateIncident', () => { + beforeAll(() => { + ServiceNowMock.mockImplementation(() => { + return { + serviceNow: { + getUserID: jest.fn().mockResolvedValue('1234'), + createIncident: jest.fn().mockResolvedValue({ id: '123', number: 'INC01' }), + updateIncident: jest.fn(), + batchAddComments: jest.fn(), + }, + }; + }); + }); + + test('create an incident without comments', async () => { + const { serviceNow } = new ServiceNowMock(); + + const res = await handleCreateIncident({ + serviceNow, + params: incident, + comments: [], + mapping: finalMapping, + }); + + expect(serviceNow.createIncident).toHaveBeenCalled(); + expect(serviceNow.createIncident).toHaveBeenCalledWith(incident); + expect(serviceNow.createIncident).toHaveReturned(); + expect(serviceNow.batchAddComments).not.toHaveBeenCalled(); + expect(res).toEqual({ id: '123', number: 'INC01' }); + }); + + test('create an incident with comments', async () => { + const { serviceNow } = new ServiceNowMock(); + + const res = await handleCreateIncident({ + serviceNow, + params: incident, + comments, + mapping: finalMapping, + }); + + expect(serviceNow.createIncident).toHaveBeenCalled(); + expect(serviceNow.createIncident).toHaveBeenCalledWith(incident); + expect(serviceNow.createIncident).toHaveReturned(); + expect(serviceNow.batchAddComments).toHaveBeenCalled(); + expect(serviceNow.batchAddComments).toHaveBeenCalledWith( + '123', + comments.map(c => c.comment), + 'comments' + ); + expect(res).toEqual({ id: '123', number: 'INC01' }); + }); + + test('update an incident without comments', async () => { + const { serviceNow } = new ServiceNowMock(); + + await handleUpdateIncident({ + incidentId: '123', + serviceNow, + params: incident, + comments: [], + mapping: finalMapping, + }); + + expect(serviceNow.updateIncident).toHaveBeenCalled(); + expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', incident); + expect(serviceNow.updateIncident).toHaveReturned(); + expect(serviceNow.batchAddComments).not.toHaveBeenCalled(); + }); + + test('update an incident with comments', async () => { + const { serviceNow } = new ServiceNowMock(); + + await handleUpdateIncident({ + incidentId: '123', + serviceNow, + params: incident, + comments, + mapping: finalMapping, + }); + + expect(serviceNow.updateIncident).toHaveBeenCalled(); + expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', incident); + expect(serviceNow.updateIncident).toHaveReturned(); + expect(serviceNow.batchAddComments).toHaveBeenCalledWith( + '123', + comments.map(c => c.comment), + 'comments' + ); + }); +}); From bfbbc3ffde8270a6a5e58198c600e0417c79d334 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 6 Mar 2020 11:06:49 +0200 Subject: [PATCH 30/42] Refactor tests --- .../servicenow/action_handlers.test.ts | 2 +- .../servicenow/helpers.test.ts | 38 +++++++++++++++---- .../builtin_action_types/servicenow/mock.ts | 35 +++++++---------- 3 files changed, 44 insertions(+), 31 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts index 245c079b59eac..ecdfa81f7139f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts @@ -7,7 +7,7 @@ import { handleCreateIncident, handleUpdateIncident } from './action_handlers'; import { ServiceNow } from '../lib/servicenow'; import { finalMapping } from './mock'; -import { Incident, UpdateIncident } from '../lib/servicenow/types'; +import { Incident } from '../lib/servicenow/types'; jest.mock('../lib/servicenow'); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts index c2f85c6790de6..efe626a416b20 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts @@ -5,8 +5,16 @@ */ import { normalizeMapping, buildMap, mapParams } from './helpers'; -import { mapping, maliciousMapping, finalMapping, params } from './mock'; +import { mapping, finalMapping } from './mock'; import { SUPPORTED_SOURCE_FIELDS } from './constants'; +import { MapsType } from './types'; + +const maliciousMapping: MapsType[] = [ + { source: '__proto__', target: 'short_description', onEditAndUpdate: 'nothing' }, + { source: 'description', target: '__proto__', onEditAndUpdate: 'nothing' }, + { source: 'comments', target: 'comments', onEditAndUpdate: 'nothing' }, + { source: 'unsupportedSource', target: 'comments', onEditAndUpdate: 'nothing' }, +]; describe('sanitizeMapping', () => { test('remove malicious fields', () => { @@ -44,18 +52,32 @@ describe('buildMap', () => { describe('mapParams', () => { test('maps params correctly', () => { - const { comments, ...restParams } = params; - const fields = mapParams(restParams, finalMapping); + const params = { + caseId: '123', + incidentId: '456', + title: 'Incident title', + description: 'Incident description', + }; + + const fields = mapParams(params, finalMapping); + expect(fields).toEqual({ - [finalMapping.get('title').target]: restParams.title, - [finalMapping.get('description').target]: restParams.description, + short_description: 'Incident title', + description: 'Incident description', }); }); test('do not add fields not in mapping', () => { - const { comments, ...restParams } = params; - const fields = mapParams(restParams, finalMapping); - const { title, description, ...unexpectedFields } = restParams; + const params = { + caseId: '123', + incidentId: '456', + title: 'Incident title', + description: 'Incident description', + }; + const fields = mapParams(params, finalMapping); + + const { title, description, ...unexpectedFields } = params; + expect(fields).not.toEqual(expect.objectContaining(unexpectedFields)); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts index 2c2aa9dbb8d5a..7f59506d87d2e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts @@ -5,7 +5,7 @@ */ import { MapsType, FinalMapping, ParamsType } from './types'; -import { Incident, IncidentResponse, UpdateIncident } from '../lib/servicenow/types'; +import { Incident } from '../lib/servicenow/types'; const mapping: MapsType[] = [ { source: 'title', target: 'short_description', onEditAndUpdate: 'nothing' }, @@ -13,37 +13,29 @@ const mapping: MapsType[] = [ { source: 'comments', target: 'comments', onEditAndUpdate: 'nothing' }, ]; -const maliciousMapping: MapsType[] = [ - { source: '__proto__', target: 'short_description', onEditAndUpdate: 'nothing' }, - { source: 'description', target: '__proto__', onEditAndUpdate: 'nothing' }, - { source: 'comments', target: 'comments', onEditAndUpdate: 'nothing' }, - { source: 'unsupportedSource', target: 'comments', onEditAndUpdate: 'nothing' }, -]; - const finalMapping: FinalMapping = new Map(); -finalMapping.set(mapping[0].source, { - target: mapping[0].target, - onEditAndUpdate: mapping[0].onEditAndUpdate, +finalMapping.set('title', { + target: 'short_description', + onEditAndUpdate: 'nothing', }); -finalMapping.set(mapping[1].source, { - target: mapping[1].target, - onEditAndUpdate: mapping[1].onEditAndUpdate, +finalMapping.set('description', { + target: 'description', + onEditAndUpdate: 'nothing', }); -finalMapping.set(mapping[2].source, { - target: mapping[2].target, - onEditAndUpdate: mapping[2].onEditAndUpdate, +finalMapping.set('comments', { + target: 'comments', + onEditAndUpdate: 'nothing', }); -finalMapping.set(mapping[0].target, { - target: mapping[0].source, - onEditAndUpdate: mapping[0].onEditAndUpdate, +finalMapping.set('short_description', { + target: 'title', + onEditAndUpdate: 'nothing', }); const params: ParamsType = { - executorAction: 'updateIncident', caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', incidentId: 'ceb5986e079f00100e48fbbf7c1ed06d', title: 'Incident title', @@ -99,7 +91,6 @@ const incident: Incident = { export { mapping, - maliciousMapping, finalMapping, params, incidentResponse, From 9535eaacdd97a7f504dcaf0b778b96a5b0ffa866 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 6 Mar 2020 16:06:28 +0200 Subject: [PATCH 31/42] Create and update comments --- .../lib/servicenow/constants.ts | 1 + .../lib/servicenow/index.ts | 60 ++++++++++++---- .../lib/servicenow/types.ts | 6 +- .../servicenow/action_handlers.ts | 68 ++++++++++++++++--- .../builtin_action_types/servicenow/index.ts | 3 +- .../builtin_action_types/servicenow/types.ts | 11 +++ 6 files changed, 123 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/constants.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/constants.ts index a2f13577dc2b2..5526fa413a44a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/constants.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/constants.ts @@ -7,3 +7,4 @@ export const API_VERSION = 'v1'; export const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`; export const USER_URL = `api/now/${API_VERSION}/table/sys_user?user_name=`; +export const COMMENT_URL = `api/now/${API_VERSION}/table/sys_journal_field`; diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.ts index b022cdba1a33c..f62e7e6455a97 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/index.ts @@ -6,13 +6,20 @@ import axios, { AxiosInstance, Method, AxiosResponse } from 'axios'; -import { INCIDENT_URL, USER_URL } from './constants'; -import { Instance, Incident, IncidentResponse, UpdateIncident } from './types'; +import { INCIDENT_URL, USER_URL, COMMENT_URL } from './constants'; +import { Instance, Incident, IncidentResponse, UpdateIncident, CommentResponse } from './types'; +import { CommentType } from '../../servicenow/types'; const validStatusCodes = [200, 201]; +const commentTemplate = { + name: 'incident', + element: 'comments', +}; + class ServiceNow { private readonly incidentUrl: string; + private readonly commentUrl: string; private readonly userUrl: string; private readonly axios: AxiosInstance; @@ -27,6 +34,7 @@ class ServiceNow { } this.incidentUrl = `${this.instance.url}/${INCIDENT_URL}`; + this.commentUrl = `${this.instance.url}/${COMMENT_URL}`; this.userUrl = `${this.instance.url}/${USER_URL}`; this.axios = axios.create({ auth: { username: this.instance.username, password: this.instance.password }, @@ -73,27 +81,53 @@ class ServiceNow { data: { ...incident }, }); - return { number: res.data.result.number, id: res.data.result.sys_id }; + return { number: res.data.result.number, incidentId: res.data.result.sys_id }; } - async updateIncident(incidentId: string, incident: UpdateIncident): Promise { - await this._patch({ + async updateIncident(incidentId: string, incident: UpdateIncident): Promise { + const res = await this._patch({ url: `${this.incidentUrl}/${incidentId}`, data: { ...incident }, }); + + return { number: res.data.result.number, incidentId: res.data.result.sys_id }; } - async batchAddComments(incidentId: string, comments: string[], field: string): Promise { - for (const comment of comments) { - await this.addComment(incidentId, comment, field); - } + async batchCreateComments( + incidentId: string, + comments: CommentType[], + field: string + ): Promise { + const res = await Promise.all(comments.map(c => this.createComment(incidentId, c, field))); + return res; } - async addComment(incidentId: string, comment: string, field: string): Promise { - await this._patch({ - url: `${this.incidentUrl}/${incidentId}`, - data: { [field]: comment }, + async batchUpdateComments(comments: CommentType[]): Promise { + const res = await Promise.all(comments.map(c => this.updateComment(c))); + return res; + } + + async createComment( + incidentId: string, + comment: CommentType, + field: string + ): Promise { + const res = await this._request({ + url: this.commentUrl, + method: 'post', + data: { ...commentTemplate, element_id: incidentId, value: comment.comment, element: field }, }); + + return { commentId: res.data.result.sys_id }; + } + + async updateComment(comment: CommentType): Promise { + const res = await this._patch({ + url: `${this.commentUrl}/${comment.incidentCommentId}`, + data: { value: comment.comment }, + }); + + return { commentId: res.data.result.sys_id }; } } diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/types.ts index 7078794d7cd9e..2f7475fc9b484 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/servicenow/types.ts @@ -18,7 +18,11 @@ export interface Incident { export interface IncidentResponse { number: string; - id: string; + incidentId: string; +} + +export interface CommentResponse { + commentId: string; } export type UpdateIncident = Partial; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts index 6b7cab5684b46..ddb168432a0ff 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts @@ -4,31 +4,46 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Incident } from '../lib/servicenow/types'; -import { ActionHandlerArguments, UpdateParamsType, UpdateActionHandlerArguments } from './types'; +import { zipWith } from 'lodash'; +import { Incident, CommentResponse } from '../lib/servicenow/types'; +import { + ActionHandlerArguments, + UpdateParamsType, + UpdateActionHandlerArguments, + IncidentCreationResponse, + CommentType, + CommentsZipped, +} from './types'; export const handleCreateIncident = async ({ serviceNow, params, comments, mapping, -}: ActionHandlerArguments) => { +}: ActionHandlerArguments): Promise => { const paramsAsIncident = params as Incident; - const { id, number } = await serviceNow.createIncident({ + const { incidentId, number } = await serviceNow.createIncident({ ...paramsAsIncident, }); + const res: IncidentCreationResponse = { incidentId, number }; + // Should return comment ID if (comments && Array.isArray(comments) && comments.length > 0) { - await serviceNow.batchAddComments( - id, - comments.map(c => c.comment), + const commentResponse = await serviceNow.batchCreateComments( + incidentId, + comments, mapping.get('comments').target ); + + res.comments = zipWith(comments, commentResponse, (a: CommentType, b: CommentResponse) => ({ + commentId: a.commentId, + incidentCommentId: b.commentId, + })); } - return { id, number }; + return { ...res }; }; export const handleUpdateIncident = async ({ @@ -40,14 +55,45 @@ export const handleUpdateIncident = async ({ }: UpdateActionHandlerArguments) => { const paramsAsIncident = params as UpdateParamsType; - await serviceNow.updateIncident(incidentId, { ...paramsAsIncident }); + const { number } = await serviceNow.updateIncident(incidentId, { + ...paramsAsIncident, + }); + + const res: IncidentCreationResponse = { incidentId, number }; // Should return comment ID if (comments && Array.isArray(comments) && comments.length > 0) { - await serviceNow.batchAddComments( + const commentsToUpdate = comments.filter(c => c.incidentCommentId); + const commentsToCreate = comments.filter(c => !c.incidentCommentId); + + const commentCreationResponse = await serviceNow.batchCreateComments( incidentId, - comments.map(c => c.comment), + commentsToCreate, mapping.get('comments').target ); + + const commentUpdateResponse = await serviceNow.batchUpdateComments(commentsToUpdate); + + const updateRes: CommentsZipped[] = zipWith( + commentsToCreate, + commentCreationResponse, + (a: CommentType, b: CommentResponse) => ({ + commentId: a.commentId, + incidentCommentId: b.commentId, + }) + ); + + const createRes: CommentsZipped[] = zipWith( + commentsToUpdate, + commentUpdateResponse, + (a: CommentType, b: CommentResponse) => ({ + commentId: a.commentId, + incidentCommentId: b.commentId, + }) + ); + + res.comments = [...updateRes, ...createRes]; } + + return { ...res }; }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index 8a357d648d20e..a0bd327792cc0 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -113,9 +113,10 @@ async function serviceNowExecutor( data, }; } else { - await handleUpdateIncident({ incidentId, ...handlerInput }); + data = await handleUpdateIncident({ incidentId, ...handlerInput }); return { ...res, + data, }; } } diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts index c378d7a47f177..6bf8a53812a8d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -44,3 +44,14 @@ export type UpdateParamsType = Partial; export type UpdateActionHandlerArguments = ActionHandlerArguments & { incidentId: string; }; + +export interface IncidentCreationResponse { + incidentId: string; + number: string; + comments?: CommentsZipped[]; +} + +export interface CommentsZipped { + commentId: string; + incidentCommentId: string; +} From d8eebee1301c24884bdcef317dab51ec2cb977b2 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 26 Feb 2020 20:39:27 +0200 Subject: [PATCH 32/42] Create servicenow connector --- .../siem/public/lib/connectors/servicenow.tsx | 174 ++++++++++++++++++ .../public/lib/connectors/translations.ts | 63 +++++++ .../siem/public/lib/connectors/types.ts | 19 ++ 3 files changed, 256 insertions(+) create mode 100644 x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx create mode 100644 x-pack/legacy/plugins/siem/public/lib/connectors/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/lib/connectors/types.ts diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx b/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx new file mode 100644 index 0000000000000..1221d81a151fd --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx @@ -0,0 +1,174 @@ +/* + * 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 React from 'react'; +import { + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiFieldPassword, +} from '@elastic/eui'; +import { + ActionConnectorFieldsProps, + ActionTypeModel, + ValidationResult, + ActionParamsProps, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../plugins/triggers_actions_ui/public/types'; + +import * as i18n from './translations'; + +import { ServiceNowActionConnector } from './types'; + +interface ServiceNowActionParams { + message: string; +} + +export function getActionType(): ActionTypeModel { + return { + id: '.servicenow', + iconClass: 'logoWebhook', + selectMessage: i18n.SERVICENOW_DESC, + actionTypeTitle: i18n.SERVICENOW_TITLE, + validateConnector: (action: ServiceNowActionConnector): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + apiUrl: [] as string[], + username: [] as string[], + password: [] as string[], + }; + + if (!action.config.apiUrl) { + errors.apiUrl.push(i18n.SERVICENOW_API_URL_REQUIRED); + } + + if (!action.secrets.username) { + errors.username.push(i18n.SERVICENOW_USERNAME_REQUIRED); + } + + if (!action.secrets.password) { + errors.password.push(i18n.SERVICENOW_PASSWORD_REQUIRED); + } + + validationResult.errors = errors; + return validationResult; + }, + validateParams: (actionParams: ServiceNowActionParams): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + message: [] as string[], + }; + validationResult.errors = errors; + return validationResult; + }, + actionConnectorFields: ServiceNowConnectorFields, + actionParamsFields: ServiceNowParamsFields, + }; +} + +const ServiceNowConnectorFields: React.FunctionComponent> = ({ action, editActionConfig, editActionSecrets, errors }) => { + const { apiUrl } = action.config; + const { username, password } = action.secrets; + + const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl !== undefined; + const isUsernameInvalid: boolean = errors.username.length > 0 && username !== undefined; + const isPasswordInvalid: boolean = errors.password.length > 0 && password !== undefined; + + return ( + <> + + + + { + editActionConfig('apiUrl', e.target.value); + }} + onBlur={() => { + if (!apiUrl) { + editActionConfig('apiUrl', ''); + } + }} + /> + + + + + + + { + editActionSecrets('username', e.target.value); + }} + onBlur={() => { + if (!username) { + editActionSecrets('username', ''); + } + }} + /> + + + + + + + { + editActionSecrets('password', e.target.value); + }} + onBlur={() => { + if (!password) { + editActionSecrets('password', ''); + } + }} + /> + + + + + ); +}; + +const ServiceNowParamsFields: React.FunctionComponent> = ({ actionParams, editAction, index, errors }) => { + return null; +}; diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/translations.ts b/x-pack/legacy/plugins/siem/public/lib/connectors/translations.ts new file mode 100644 index 0000000000000..40a5ad7e3cdc6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/lib/connectors/translations.ts @@ -0,0 +1,63 @@ +/* + * 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 SERVICENOW_DESC = i18n.translate( + 'xpack.siem.case.connectors.servicenow.selectMessageText', + { + defaultMessage: 'Push or update SIEM case data to a new incident in ServiceNow', + } +); + +export const SERVICENOW_TITLE = i18n.translate( + 'xpack.siem.case.connectors.servicenow.actionTypeTitle', + { + defaultMessage: 'ServiceNow', + } +); + +export const SERVICENOW_API_URL_LABEL = i18n.translate( + 'xpack.siem.case.connectors.servicenow.apiUrlTextFieldLabel', + { + defaultMessage: 'URL', + } +); + +export const SERVICENOW_API_URL_REQUIRED = i18n.translate( + 'xpack.siem.case.connectors.servicenow.requiredApiUrlTextField', + { + defaultMessage: 'URL is required', + } +); + +export const SERVICENOW_USERNAME_LABEL = i18n.translate( + 'xpack.siem.case.connectors.servicenow.usernameTextFieldLabel', + { + defaultMessage: 'Username', + } +); + +export const SERVICENOW_USERNAME_REQUIRED = i18n.translate( + 'xpack.siem.case.connectors.servicenow.requiredUsernameTextField', + { + defaultMessage: 'Username is required', + } +); + +export const SERVICENOW_PASSWORD_LABEL = i18n.translate( + 'xpack.siem.case.connectors.servicenow.passwordTextFieldLabel', + { + defaultMessage: 'Password', + } +); + +export const SERVICENOW_PASSWORD_REQUIRED = i18n.translate( + 'xpack.siem.case.connectors.servicenow.requiredPasswordTextField', + { + defaultMessage: 'Password is required', + } +); diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/types.ts b/x-pack/legacy/plugins/siem/public/lib/connectors/types.ts new file mode 100644 index 0000000000000..7522c1ae1250f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/lib/connectors/types.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +interface ServiceNowConfig { + apiUrl: string; +} + +interface ServiceNowSecrets { + username: string; + password: string; +} + +export interface ServiceNowActionConnector { + config: ServiceNowConfig; + secrets: ServiceNowSecrets; +} From c76dc04e589e29c961fdd33e8b97169ead619054 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 26 Feb 2020 20:39:49 +0200 Subject: [PATCH 33/42] Register servicenow connector --- .../legacy/plugins/siem/public/lib/connectors/index.ts | 7 +++++++ x-pack/legacy/plugins/siem/public/plugin.tsx | 9 +++++++++ 2 files changed, 16 insertions(+) create mode 100644 x-pack/legacy/plugins/siem/public/lib/connectors/index.ts diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/index.ts b/x-pack/legacy/plugins/siem/public/lib/connectors/index.ts new file mode 100644 index 0000000000000..fdf337b5ef120 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/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 serviceNowActionType } from './servicenow'; diff --git a/x-pack/legacy/plugins/siem/public/plugin.tsx b/x-pack/legacy/plugins/siem/public/plugin.tsx index 8be5510cda83a..c1921ba95d661 100644 --- a/x-pack/legacy/plugins/siem/public/plugin.tsx +++ b/x-pack/legacy/plugins/siem/public/plugin.tsx @@ -21,6 +21,13 @@ import { UsageCollectionSetup } from '../../../../../src/plugins/usage_collectio import { initTelemetry } from './lib/telemetry'; import { KibanaServices } from './lib/kibana'; +import { serviceNowActionType } from './lib/connectors'; + +import { + TriggersAndActionsUIPublicPluginSetup, + TriggersAndActionsUIPublicPluginStart, +} from '../../../../plugins/triggers_actions_ui/public'; + export { AppMountParameters, CoreSetup, CoreStart, PluginInitializerContext }; export interface SetupPlugins { @@ -59,6 +66,8 @@ export class Plugin implements IPlugin { const [coreStart, startPlugins] = await core.getStartServices(); const { renderApp } = await import('./app'); + plugins.triggers_actions_ui.actionTypeRegistry.register(serviceNowActionType()); + return renderApp(coreStart, startPlugins as StartPlugins, params); }, }); From ac7afa394f46d443d76fb6c2cf01080be9147a08 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 27 Feb 2020 15:04:03 +0200 Subject: [PATCH 34/42] Add ServiceNow logo --- .../plugins/siem/public/lib/connectors/logos/servicenow.svg | 5 +++++ .../legacy/plugins/siem/public/lib/connectors/servicenow.tsx | 4 +++- .../case/components/configure_cases/connectors_dropdown.tsx | 4 +++- 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100755 x-pack/legacy/plugins/siem/public/lib/connectors/logos/servicenow.svg diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/logos/servicenow.svg b/x-pack/legacy/plugins/siem/public/lib/connectors/logos/servicenow.svg new file mode 100755 index 0000000000000..dcd022a8dca18 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/lib/connectors/logos/servicenow.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx b/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx index 1221d81a151fd..cc86e7bceb405 100644 --- a/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx +++ b/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx @@ -23,6 +23,8 @@ import * as i18n from './translations'; import { ServiceNowActionConnector } from './types'; +import logo from './logos/servicenow.svg'; + interface ServiceNowActionParams { message: string; } @@ -30,7 +32,7 @@ interface ServiceNowActionParams { export function getActionType(): ActionTypeModel { return { id: '.servicenow', - iconClass: 'logoWebhook', + iconClass: logo, selectMessage: i18n.SERVICENOW_DESC, actionTypeTitle: i18n.SERVICENOW_TITLE, validateConnector: (action: ServiceNowActionConnector): ValidationResult => { diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx index d43935deda395..2de3aaa719211 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx @@ -10,6 +10,8 @@ import styled from 'styled-components'; import * as i18n from './translations'; +import serviceNowLogo from '../../../../../lib/connectors/logos/servicenow.svg'; + const ICON_SIZE = 'm'; const EuiIconExtended = styled(EuiIcon)` @@ -31,7 +33,7 @@ const connectors: Array> = [ value: 'servicenow-connector', inputDisplay: ( <> - + {'My ServiceNow connector'} ), From 512698ed90ae93d149890c26ad6e1f16eb352e2d Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 27 Feb 2020 17:11:20 +0200 Subject: [PATCH 35/42] Create connnectors mapping --- .../siem/public/lib/connectors/config.ts | 17 +++++++++++++++++ .../plugins/siem/public/lib/connectors/types.ts | 5 +++++ 2 files changed, 22 insertions(+) create mode 100644 x-pack/legacy/plugins/siem/public/lib/connectors/config.ts diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/config.ts b/x-pack/legacy/plugins/siem/public/lib/connectors/config.ts new file mode 100644 index 0000000000000..0744675997d9c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/lib/connectors/config.ts @@ -0,0 +1,17 @@ +/* + * 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'; +import serviceNowLogo from './logos/servicenow.svg'; + +const connectors = new Map(); + +connectors.set('.servicenow', { + actionTypeId: '.servicenow', + logo: serviceNowLogo, +}); + +export { connectors }; diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/types.ts b/x-pack/legacy/plugins/siem/public/lib/connectors/types.ts index 7522c1ae1250f..27dfede8d0e41 100644 --- a/x-pack/legacy/plugins/siem/public/lib/connectors/types.ts +++ b/x-pack/legacy/plugins/siem/public/lib/connectors/types.ts @@ -17,3 +17,8 @@ export interface ServiceNowActionConnector { config: ServiceNowConfig; secrets: ServiceNowSecrets; } + +export interface Connector { + actionTypeId: string; + logo: string; +} From 3c1ab93cac844b7eaeccf2cc50960ee6e41d22d8 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 27 Feb 2020 17:14:22 +0200 Subject: [PATCH 36/42] Create validators in utils --- .../siem/public/utils/validators/index.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 x-pack/legacy/plugins/siem/public/utils/validators/index.ts diff --git a/x-pack/legacy/plugins/siem/public/utils/validators/index.ts b/x-pack/legacy/plugins/siem/public/utils/validators/index.ts new file mode 100644 index 0000000000000..99b01c8b22974 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/utils/validators/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { isEmpty } from 'lodash/fp'; + +const urlExpression = /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/gi; + +export const isUrlInvalid = (url: string | null | undefined) => { + if (!isEmpty(url) && url != null && url.match(urlExpression) == null) { + return true; + } + return false; +}; From e29a9a449862faf4799721994835d4a64b1d1cd0 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 27 Feb 2020 17:15:53 +0200 Subject: [PATCH 37/42] Use validators in connectors --- .../plugins/siem/public/lib/connectors/validators.ts | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 x-pack/legacy/plugins/siem/public/lib/connectors/validators.ts diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/validators.ts b/x-pack/legacy/plugins/siem/public/lib/connectors/validators.ts new file mode 100644 index 0000000000000..2989cf4d98f85 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/lib/connectors/validators.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 { isUrlInvalid } from '../../utils/validators'; From baa848105fb94786ab0981234c815ac04666323f Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 27 Feb 2020 17:17:55 +0200 Subject: [PATCH 38/42] Validate URL --- .../plugins/siem/public/lib/connectors/servicenow.tsx | 5 +++++ .../plugins/siem/public/lib/connectors/translations.ts | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx b/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx index cc86e7bceb405..3124ea49cd7c5 100644 --- a/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx +++ b/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx @@ -22,6 +22,7 @@ import { import * as i18n from './translations'; import { ServiceNowActionConnector } from './types'; +import { isUrlInvalid } from './validators'; import logo from './logos/servicenow.svg'; @@ -47,6 +48,10 @@ export function getActionType(): ActionTypeModel { errors.apiUrl.push(i18n.SERVICENOW_API_URL_REQUIRED); } + if (isUrlInvalid(action.config.apiUrl)) { + errors.apiUrl.push(i18n.SERVICENOW_API_URL_INVALID); + } + if (!action.secrets.username) { errors.username.push(i18n.SERVICENOW_USERNAME_REQUIRED); } diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/translations.ts b/x-pack/legacy/plugins/siem/public/lib/connectors/translations.ts index 40a5ad7e3cdc6..ae2084120255c 100644 --- a/x-pack/legacy/plugins/siem/public/lib/connectors/translations.ts +++ b/x-pack/legacy/plugins/siem/public/lib/connectors/translations.ts @@ -34,6 +34,13 @@ export const SERVICENOW_API_URL_REQUIRED = i18n.translate( } ); +export const SERVICENOW_API_URL_INVALID = i18n.translate( + 'xpack.siem.case.connectors.servicenow.invalidApiUrlTextField', + { + defaultMessage: 'URL is invalid', + } +); + export const SERVICENOW_USERNAME_LABEL = i18n.translate( 'xpack.siem.case.connectors.servicenow.usernameTextFieldLabel', { From 379e693f93c2c28c22ff795619cf3f1a5c77da7e Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 27 Feb 2020 17:21:00 +0200 Subject: [PATCH 39/42] Use connectors from config --- .../plugins/siem/public/lib/connectors/servicenow.tsx | 8 +++++--- .../components/configure_cases/connectors_dropdown.tsx | 5 +++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx b/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx index 3124ea49cd7c5..365d3ca9ef5e6 100644 --- a/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx +++ b/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx @@ -24,7 +24,9 @@ import * as i18n from './translations'; import { ServiceNowActionConnector } from './types'; import { isUrlInvalid } from './validators'; -import logo from './logos/servicenow.svg'; +import { connectors } from './config'; + +const serviceNowDefinition = connectors.get('.servicenow')!; interface ServiceNowActionParams { message: string; @@ -32,8 +34,8 @@ interface ServiceNowActionParams { export function getActionType(): ActionTypeModel { return { - id: '.servicenow', - iconClass: logo, + id: serviceNowDefinition.actionTypeId, + iconClass: serviceNowDefinition.logo, selectMessage: i18n.SERVICENOW_DESC, actionTypeTitle: i18n.SERVICENOW_TITLE, validateConnector: (action: ServiceNowActionConnector): ValidationResult => { diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx index 2de3aaa719211..3c88afe186bb0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx @@ -10,9 +10,10 @@ import styled from 'styled-components'; import * as i18n from './translations'; -import serviceNowLogo from '../../../../../lib/connectors/logos/servicenow.svg'; +import { connectors as connectorsDefinition } from '../../../../lib/connectors/config'; const ICON_SIZE = 'm'; +const serviceNowDefinition = connectorsDefinition.get('.servicenow')!; const EuiIconExtended = styled(EuiIcon)` margin-right: 13px; @@ -33,7 +34,7 @@ const connectors: Array> = [ value: 'servicenow-connector', inputDisplay: ( <> - + {'My ServiceNow connector'} ), From 83523db2e7656a97951b5115ad7688c6cda4a6e6 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 6 Mar 2020 11:55:50 +0200 Subject: [PATCH 40/42] Enable triggers_aciton_ui plugin --- x-pack/legacy/plugins/siem/index.ts | 2 +- x-pack/legacy/plugins/siem/public/plugin.tsx | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/siem/index.ts b/x-pack/legacy/plugins/siem/index.ts index db398821aecfd..3773283555b32 100644 --- a/x-pack/legacy/plugins/siem/index.ts +++ b/x-pack/legacy/plugins/siem/index.ts @@ -40,7 +40,7 @@ export const siem = (kibana: any) => { id: APP_ID, configPrefix: 'xpack.siem', publicDir: resolve(__dirname, 'public'), - require: ['kibana', 'elasticsearch', 'alerting', 'actions'], + require: ['kibana', 'elasticsearch', 'alerting', 'actions', 'triggers_actions_ui'], uiExports: { app: { description: i18n.translate('xpack.siem.securityDescription', { diff --git a/x-pack/legacy/plugins/siem/public/plugin.tsx b/x-pack/legacy/plugins/siem/public/plugin.tsx index c1921ba95d661..f22add59a95d4 100644 --- a/x-pack/legacy/plugins/siem/public/plugin.tsx +++ b/x-pack/legacy/plugins/siem/public/plugin.tsx @@ -33,6 +33,7 @@ export { AppMountParameters, CoreSetup, CoreStart, PluginInitializerContext }; export interface SetupPlugins { home: HomePublicPluginSetup; usageCollection: UsageCollectionSetup; + triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; } export interface StartPlugins { data: DataPublicPluginStart; @@ -40,6 +41,7 @@ export interface StartPlugins { inspector: InspectorStart; newsfeed?: NewsfeedStart; uiActions: UiActionsStart; + triggers_actions_ui: TriggersAndActionsUIPublicPluginStart; } export type StartServices = CoreStart & StartPlugins; From 7e42a5d1cd2f168280e4c27aa57abcfb26d01085 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 6 Mar 2020 11:56:19 +0200 Subject: [PATCH 41/42] Show flyout --- .../components/configure_cases/connectors.tsx | 46 ++++++++++++++----- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx index 561464e44c703..4146bfef291b6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useState } from 'react'; import { EuiDescribedFormGroup, EuiFormRow, @@ -18,6 +18,12 @@ import styled from 'styled-components'; import { ConnectorsDropdown } from './connectors_dropdown'; import * as i18n from './translations'; +import { + ActionsConnectorsContextProvider, + ConnectorAddFlyout, +} from '../../../../../../../../plugins/triggers_actions_ui/public'; +import { useKibana } from '../../../../lib/kibana'; + const EuiFormRowExtended = styled(EuiFormRow)` .euiFormRow__labelWrapper { .euiFormRow__label { @@ -27,25 +33,43 @@ const EuiFormRowExtended = styled(EuiFormRow)` `; const ConnectorsComponent: React.FC = () => { + const { http, triggers_actions_ui, notifications, application } = useKibana().services; + const [addFlyoutVisible, setAddFlyoutVisibility] = useState(false); + const dropDownLabel = ( {i18n.INCIDENT_MANAGEMENT_SYSTEM_LABEL} - {i18n.ADD_NEW_CONNECTOR} + setAddFlyoutVisibility(true)}>{i18n.ADD_NEW_CONNECTOR} ); return ( - {i18n.INCIDENT_MANAGEMENT_SYSTEM_TITLE}} - description={i18n.INCIDENT_MANAGEMENT_SYSTEM_DESC} - > - - - - + <> + {i18n.INCIDENT_MANAGEMENT_SYSTEM_TITLE}} + description={i18n.INCIDENT_MANAGEMENT_SYSTEM_DESC} + > + + + + + + + + ); }; From 6449515900bd445c39b83f5fe1d8966c66dd59e9 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 6 Mar 2020 14:31:32 +0200 Subject: [PATCH 42/42] Add closures options --- .../siem/public/lib/connectors/servicenow.tsx | 59 ++++++++++++++++++- .../siem/public/lib/connectors/types.ts | 3 + 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx b/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx index 365d3ca9ef5e6..a2733322acdf5 100644 --- a/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx +++ b/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx @@ -10,6 +10,7 @@ import { EuiFlexItem, EuiFormRow, EuiFieldPassword, + EuiRadioGroup, } from '@elastic/eui'; import { ActionConnectorFieldsProps, @@ -81,13 +82,53 @@ export function getActionType(): ActionTypeModel { const ServiceNowConnectorFields: React.FunctionComponent> = ({ action, editActionConfig, editActionSecrets, errors }) => { - const { apiUrl } = action.config; + const { apiUrl, casesConfiguration } = action.config; const { username, password } = action.secrets; + const closure = casesConfiguration?.closure ?? 'manual'; + const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl !== undefined; const isUsernameInvalid: boolean = errors.username.length > 0 && username !== undefined; const isPasswordInvalid: boolean = errors.password.length > 0 && password !== undefined; + if (!casesConfiguration) { + editActionConfig('casesConfiguration', { + closure: 'manual', + mapping: [ + { + source: 'title', + target: 'description', + onEditAndUpdate: 'nothing', + }, + { + source: 'description', + target: 'short_description', + onEditAndUpdate: 'nothing', + }, + { + source: 'comments', + target: 'work_notes', + onEditAndUpdate: 'nothing', + }, + ], + }); + } + + const radios = [ + { + id: 'manual', + label: 'Manually close SIEM cases', + }, + { + id: 'newIncident', + label: 'Automatically close SIEM cases when pushing new incident to third-party', + }, + { + id: 'closedIncident', + label: 'Automatically close SIEM cases when incident is closed in third-party', + }, + ]; + return ( <> @@ -105,7 +146,7 @@ const ServiceNowConnectorFields: React.FunctionComponent { editActionConfig('apiUrl', e.target.value); }} @@ -172,6 +213,20 @@ const ServiceNowConnectorFields: React.FunctionComponent + + + + { + editActionConfig('casesConfiguration', { ...casesConfiguration, closure: val }); + }} + name="closure" + /> + + + ); }; diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/types.ts b/x-pack/legacy/plugins/siem/public/lib/connectors/types.ts index 27dfede8d0e41..a8f5e47e05d72 100644 --- a/x-pack/legacy/plugins/siem/public/lib/connectors/types.ts +++ b/x-pack/legacy/plugins/siem/public/lib/connectors/types.ts @@ -6,6 +6,9 @@ interface ServiceNowConfig { apiUrl: string; + casesConfiguration: { + closure: string; + }; } interface ServiceNowSecrets {