mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Alerts] ServiceNow SIR Connector (#88190)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
bdca03dcfd
commit
7a45fc45e1
46 changed files with 2265 additions and 444 deletions
|
@ -78,7 +78,7 @@ pageLoadAssetSize:
|
|||
tileMap: 65337
|
||||
timelion: 29920
|
||||
transform: 41007
|
||||
triggersActionsUi: 170001
|
||||
triggersActionsUi: 186732
|
||||
uiActions: 97717
|
||||
uiActionsEnhanced: 313011
|
||||
upgradeAssistant: 81241
|
||||
|
|
|
@ -70,12 +70,14 @@ Table of Contents
|
|||
- [`params`](#params-6)
|
||||
- [`subActionParams (pushToService)`](#subactionparams-pushtoservice)
|
||||
- [`subActionParams (getFields)`](#subactionparams-getfields)
|
||||
- [`subActionParams (getIncident)`](#subactionparams-getincident)
|
||||
- [`subActionParams (getChoices)`](#subactionparams-getchoices)
|
||||
- [Jira](#jira)
|
||||
- [`config`](#config-7)
|
||||
- [`secrets`](#secrets-7)
|
||||
- [`params`](#params-7)
|
||||
- [`subActionParams (pushToService)`](#subactionparams-pushtoservice-1)
|
||||
- [`subActionParams (getIncident)`](#subactionparams-getincident)
|
||||
- [`subActionParams (getIncident)`](#subactionparams-getincident-1)
|
||||
- [`subActionParams (issueTypes)`](#subactionparams-issuetypes)
|
||||
- [`subActionParams (fieldsByIssueType)`](#subactionparams-fieldsbyissuetype)
|
||||
- [`subActionParams (issues)`](#subactionparams-issues)
|
||||
|
@ -347,17 +349,18 @@ const result = await actionsClient.execute({
|
|||
|
||||
Kibana ships with a set of built-in action types:
|
||||
|
||||
| Type | Id | Description |
|
||||
| ------------------------------- | ------------- | ------------------------------------------------------------------ |
|
||||
| [Server log](#server-log) | `.server-log` | Logs messages to the Kibana log using Kibana's logger |
|
||||
| [Email](#email) | `.email` | Sends an email using SMTP |
|
||||
| [Slack](#slack) | `.slack` | Posts a message to a slack channel |
|
||||
| [Index](#index) | `.index` | Indexes document(s) into Elasticsearch |
|
||||
| [Webhook](#webhook) | `.webhook` | Send a payload to a web service using HTTP POST or PUT |
|
||||
| [PagerDuty](#pagerduty) | `.pagerduty` | Trigger, resolve, or acknowlege an incident to a PagerDuty service |
|
||||
| [ServiceNow](#servicenow) | `.servicenow` | Create or update an incident to a ServiceNow instance |
|
||||
| [Jira](#jira) | `.jira` | Create or update an issue to a Jira instance |
|
||||
| [IBM Resilient](#ibm-resilient) | `.resilient` | Create or update an incident to a IBM Resilient instance |
|
||||
| Type | Id | Description |
|
||||
| ------------------------------- | ----------------- | ------------------------------------------------------------------ |
|
||||
| [Server log](#server-log) | `.server-log` | Logs messages to the Kibana log using Kibana's logger |
|
||||
| [Email](#email) | `.email` | Sends an email using SMTP |
|
||||
| [Slack](#slack) | `.slack` | Posts a message to a slack channel |
|
||||
| [Index](#index) | `.index` | Indexes document(s) into Elasticsearch |
|
||||
| [Webhook](#webhook) | `.webhook` | Send a payload to a web service using HTTP POST or PUT |
|
||||
| [PagerDuty](#pagerduty) | `.pagerduty` | Trigger, resolve, or acknowlege an incident to a PagerDuty service |
|
||||
| [ServiceNow ITSM](#servicenow) | `.servicenow` | Create or update an incident to a ServiceNow ITSM instance |
|
||||
| [ServiceNow SIR](#servicenow) | `.servicenow-sir` | Create or update an incident to a ServiceNow SIR instance |
|
||||
| [Jira](#jira) | `.jira` | Create or update an issue to a Jira instance |
|
||||
| [IBM Resilient](#ibm-resilient) | `.resilient` | Create or update an incident to a IBM Resilient instance |
|
||||
|
||||
---
|
||||
|
||||
|
@ -549,9 +552,11 @@ For more details see [PagerDuty v2 event parameters](https://v2.developer.pagerd
|
|||
|
||||
## ServiceNow
|
||||
|
||||
ID: `.servicenow`
|
||||
ServiceNow ITSM ID: `.servicenow`
|
||||
|
||||
The ServiceNow action uses the [V2 Table API](https://developer.servicenow.com/app.do#!/rest_api_doc?v=orlando&id=c_TableAPI) to create and update ServiceNow incidents.
|
||||
ServiceNow SIR ID: `.servicenow-sir`
|
||||
|
||||
The ServiceNow actions use the [V2 Table API](https://developer.servicenow.com/app.do#!/rest_api_doc?v=orlando&id=c_TableAPI) to create and update ServiceNow incidents. Both action types use the same `config`, `secrets`, and `params` schema.
|
||||
|
||||
### `config`
|
||||
|
||||
|
@ -568,10 +573,10 @@ The ServiceNow action uses the [V2 Table API](https://developer.servicenow.com/a
|
|||
|
||||
### `params`
|
||||
|
||||
| Property | Description | Type |
|
||||
| --------------- | --------------------------------------------------------------------- | ------ |
|
||||
| subAction | The sub action to perform. It can be `getFields`, and `pushToService` | string |
|
||||
| subActionParams | The parameters of the sub action | object |
|
||||
| Property | Description | Type |
|
||||
| --------------- | -------------------------------------------------------------------------------------------------- | ------ |
|
||||
| subAction | The sub action to perform. It can be `pushToService`, `getFields`, `getIncident`, and `getChoices` | string |
|
||||
| subActionParams | The parameters of the sub action | object |
|
||||
|
||||
#### `subActionParams (pushToService)`
|
||||
|
||||
|
@ -595,6 +600,19 @@ The following table describes the properties of the `incident` object.
|
|||
|
||||
No parameters for `getFields` sub-action. Provide an empty object `{}`.
|
||||
|
||||
#### `subActionParams (getIncident)`
|
||||
|
||||
| Property | Description | Type |
|
||||
| ---------- | ------------------------------------- | ------ |
|
||||
| externalId | The id of the incident in ServiceNow. | string |
|
||||
|
||||
|
||||
#### `subActionParams (getChoices)`
|
||||
|
||||
| Property | Description | Type |
|
||||
| -------- | ------------------------------------------------------------ | -------- |
|
||||
| fields | An array of fields. Example: `[priority, category, impact]`. | string[] |
|
||||
|
||||
---
|
||||
|
||||
## Jira
|
||||
|
|
|
@ -14,7 +14,7 @@ import { getActionType as getPagerDutyActionType } from './pagerduty';
|
|||
import { getActionType as getServerLogActionType } from './server_log';
|
||||
import { getActionType as getSlackActionType } from './slack';
|
||||
import { getActionType as getWebhookActionType } from './webhook';
|
||||
import { getActionType as getServiceNowActionType } from './servicenow';
|
||||
import { getServiceNowITSMActionType, getServiceNowSIRActionType } from './servicenow';
|
||||
import { getActionType as getJiraActionType } from './jira';
|
||||
import { getActionType as getResilientActionType } from './resilient';
|
||||
import { getActionType as getTeamsActionType } from './teams';
|
||||
|
@ -38,7 +38,8 @@ export {
|
|||
} from './webhook';
|
||||
export {
|
||||
ActionParamsType as ServiceNowActionParams,
|
||||
ActionTypeId as ServiceNowActionTypeId,
|
||||
ServiceNowITSMActionTypeId,
|
||||
ServiceNowSIRActionTypeId,
|
||||
} from './servicenow';
|
||||
export { ActionParamsType as JiraActionParams, ActionTypeId as JiraActionTypeId } from './jira';
|
||||
export {
|
||||
|
@ -66,7 +67,8 @@ export function registerBuiltInActionTypes({
|
|||
actionTypeRegistry.register(getServerLogActionType({ logger }));
|
||||
actionTypeRegistry.register(getSlackActionType({ logger, configurationUtilities }));
|
||||
actionTypeRegistry.register(getWebhookActionType({ logger, configurationUtilities }));
|
||||
actionTypeRegistry.register(getServiceNowActionType({ logger, configurationUtilities }));
|
||||
actionTypeRegistry.register(getServiceNowITSMActionType({ logger, configurationUtilities }));
|
||||
actionTypeRegistry.register(getServiceNowSIRActionType({ logger, configurationUtilities }));
|
||||
actionTypeRegistry.register(getJiraActionType({ logger, configurationUtilities }));
|
||||
actionTypeRegistry.register(getResilientActionType({ logger, configurationUtilities }));
|
||||
actionTypeRegistry.register(getTeamsActionType({ logger, configurationUtilities }));
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
import { externalServiceMock, apiParams, serviceNowCommonFields } from './mocks';
|
||||
import { externalServiceMock, apiParams, serviceNowCommonFields, serviceNowChoices } from './mocks';
|
||||
import { ExternalService } from './types';
|
||||
import { api } from './api';
|
||||
let mockedLogger: jest.Mocked<Logger>;
|
||||
|
@ -235,4 +235,14 @@ describe('api', () => {
|
|||
expect(res).toEqual(serviceNowCommonFields);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getChoices', () => {
|
||||
test('it returns the fields correctly', async () => {
|
||||
const res = await api.getChoices({
|
||||
externalService,
|
||||
params: { fields: ['priority'] },
|
||||
});
|
||||
expect(res).toEqual(serviceNowChoices);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
*/
|
||||
import {
|
||||
ExternalServiceApi,
|
||||
GetChoicesHandlerArgs,
|
||||
GetChoicesResponse,
|
||||
GetCommonFieldsHandlerArgs,
|
||||
GetCommonFieldsResponse,
|
||||
GetIncidentApiHandlerArgs,
|
||||
|
@ -71,7 +73,16 @@ const getFieldsHandler = async ({
|
|||
return res;
|
||||
};
|
||||
|
||||
const getChoicesHandler = async ({
|
||||
externalService,
|
||||
params,
|
||||
}: GetChoicesHandlerArgs): Promise<GetChoicesResponse> => {
|
||||
const res = await externalService.getChoices(params.fields);
|
||||
return res;
|
||||
};
|
||||
|
||||
export const api: ExternalServiceApi = {
|
||||
getChoices: getChoicesHandler,
|
||||
getFields: getFieldsHandler,
|
||||
getIncident: getIncidentHandler,
|
||||
handshake: handshakeHandler,
|
||||
|
|
|
@ -11,7 +11,8 @@ import { validate } from './validators';
|
|||
import {
|
||||
ExternalIncidentServiceConfiguration,
|
||||
ExternalIncidentServiceSecretConfiguration,
|
||||
ExecutorParamsSchema,
|
||||
ExecutorParamsSchemaITSM,
|
||||
ExecutorParamsSchemaSIR,
|
||||
} from './schema';
|
||||
import { ActionsConfigurationUtilities } from '../../actions_config';
|
||||
import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../../types';
|
||||
|
@ -27,18 +28,26 @@ import {
|
|||
PushToServiceResponse,
|
||||
ExecutorSubActionCommonFieldsParams,
|
||||
ServiceNowExecutorResultData,
|
||||
ExecutorSubActionGetChoicesParams,
|
||||
} from './types';
|
||||
|
||||
export type ActionParamsType = TypeOf<typeof ExecutorParamsSchema>;
|
||||
export type ActionParamsType =
|
||||
| TypeOf<typeof ExecutorParamsSchemaITSM>
|
||||
| TypeOf<typeof ExecutorParamsSchemaSIR>;
|
||||
|
||||
interface GetActionTypeParams {
|
||||
logger: Logger;
|
||||
configurationUtilities: ActionsConfigurationUtilities;
|
||||
}
|
||||
|
||||
export const ActionTypeId = '.servicenow';
|
||||
const serviceNowITSMTable = 'incident';
|
||||
const serviceNowSIRTable = 'sn_si_incident';
|
||||
|
||||
export const ServiceNowITSMActionTypeId = '.servicenow';
|
||||
export const ServiceNowSIRActionTypeId = '.servicenow-sir';
|
||||
|
||||
// action type definition
|
||||
export function getActionType(
|
||||
export function getServiceNowITSMActionType(
|
||||
params: GetActionTypeParams
|
||||
): ActionType<
|
||||
ServiceNowPublicConfigurationType,
|
||||
|
@ -48,9 +57,9 @@ export function getActionType(
|
|||
> {
|
||||
const { logger, configurationUtilities } = params;
|
||||
return {
|
||||
id: ActionTypeId,
|
||||
id: ServiceNowITSMActionTypeId,
|
||||
minimumLicenseRequired: 'platinum',
|
||||
name: i18n.NAME,
|
||||
name: i18n.SERVICENOW_ITSM,
|
||||
validate: {
|
||||
config: schema.object(ExternalIncidentServiceConfiguration, {
|
||||
validate: curry(validate.config)(configurationUtilities),
|
||||
|
@ -58,19 +67,46 @@ export function getActionType(
|
|||
secrets: schema.object(ExternalIncidentServiceSecretConfiguration, {
|
||||
validate: curry(validate.secrets)(configurationUtilities),
|
||||
}),
|
||||
params: ExecutorParamsSchema,
|
||||
params: ExecutorParamsSchemaITSM,
|
||||
},
|
||||
executor: curry(executor)({ logger, configurationUtilities }),
|
||||
executor: curry(executor)({ logger, configurationUtilities, table: serviceNowITSMTable }),
|
||||
};
|
||||
}
|
||||
|
||||
export function getServiceNowSIRActionType(
|
||||
params: GetActionTypeParams
|
||||
): ActionType<
|
||||
ServiceNowPublicConfigurationType,
|
||||
ServiceNowSecretConfigurationType,
|
||||
ExecutorParams,
|
||||
PushToServiceResponse | {}
|
||||
> {
|
||||
const { logger, configurationUtilities } = params;
|
||||
return {
|
||||
id: ServiceNowSIRActionTypeId,
|
||||
minimumLicenseRequired: 'platinum',
|
||||
name: i18n.SERVICENOW_SIR,
|
||||
validate: {
|
||||
config: schema.object(ExternalIncidentServiceConfiguration, {
|
||||
validate: curry(validate.config)(configurationUtilities),
|
||||
}),
|
||||
secrets: schema.object(ExternalIncidentServiceSecretConfiguration, {
|
||||
validate: curry(validate.secrets)(configurationUtilities),
|
||||
}),
|
||||
params: ExecutorParamsSchemaSIR,
|
||||
},
|
||||
executor: curry(executor)({ logger, configurationUtilities, table: serviceNowSIRTable }),
|
||||
};
|
||||
}
|
||||
|
||||
// action executor
|
||||
const supportedSubActions: string[] = ['getFields', 'pushToService'];
|
||||
const supportedSubActions: string[] = ['getFields', 'pushToService', 'getChoices', 'getIncident'];
|
||||
async function executor(
|
||||
{
|
||||
logger,
|
||||
configurationUtilities,
|
||||
}: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities },
|
||||
table,
|
||||
}: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities; table: string },
|
||||
execOptions: ActionTypeExecutorOptions<
|
||||
ServiceNowPublicConfigurationType,
|
||||
ServiceNowSecretConfigurationType,
|
||||
|
@ -82,6 +118,7 @@ async function executor(
|
|||
let data: ServiceNowExecutorResultData | null = null;
|
||||
|
||||
const externalService = createExternalService(
|
||||
table,
|
||||
{
|
||||
config,
|
||||
secrets,
|
||||
|
@ -122,5 +159,13 @@ async function executor(
|
|||
});
|
||||
}
|
||||
|
||||
if (subAction === 'getChoices') {
|
||||
const getChoicesParams = subActionParams as ExecutorSubActionGetChoicesParams;
|
||||
data = await api.getChoices({
|
||||
externalService,
|
||||
params: getChoicesParams,
|
||||
});
|
||||
}
|
||||
|
||||
return { status: 'ok', data: data ?? {}, actionId };
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ExternalService, PushToServiceApiParams, ExecutorSubActionPushParams } from './types';
|
||||
import { ExternalService, ExecutorSubActionPushParams } from './types';
|
||||
|
||||
export const serviceNowCommonFields = [
|
||||
{
|
||||
|
@ -33,8 +33,43 @@ export const serviceNowCommonFields = [
|
|||
element: 'sys_updated_by',
|
||||
},
|
||||
];
|
||||
|
||||
export const serviceNowChoices = [
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '1 - Critical',
|
||||
value: '1',
|
||||
element: 'priority',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '2 - High',
|
||||
value: '2',
|
||||
element: 'priority',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '3 - Moderate',
|
||||
value: '3',
|
||||
element: 'priority',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '4 - Low',
|
||||
value: '4',
|
||||
element: 'priority',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '5 - Planning',
|
||||
value: '5',
|
||||
element: 'priority',
|
||||
},
|
||||
];
|
||||
|
||||
const createMock = (): jest.Mocked<ExternalService> => {
|
||||
const service = {
|
||||
getChoices: jest.fn().mockImplementation(() => Promise.resolve(serviceNowChoices)),
|
||||
getFields: jest.fn().mockImplementation(() => Promise.resolve(serviceNowCommonFields)),
|
||||
getIncident: jest.fn().mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
|
@ -89,8 +124,6 @@ const executorParams: ExecutorSubActionPushParams = {
|
|||
],
|
||||
};
|
||||
|
||||
const apiParams: PushToServiceApiParams = {
|
||||
...executorParams,
|
||||
};
|
||||
const apiParams = executorParams;
|
||||
|
||||
export { externalServiceMock, executorParams, apiParams };
|
||||
|
|
|
@ -28,25 +28,48 @@ export const ExecutorSubActionSchema = schema.oneOf([
|
|||
schema.literal('getIncident'),
|
||||
schema.literal('pushToService'),
|
||||
schema.literal('handshake'),
|
||||
schema.literal('getChoices'),
|
||||
]);
|
||||
|
||||
export const ExecutorSubActionPushParamsSchema = schema.object({
|
||||
const CommentsSchema = schema.nullable(
|
||||
schema.arrayOf(
|
||||
schema.object({
|
||||
comment: schema.string(),
|
||||
commentId: schema.string(),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const CommonAttributes = {
|
||||
short_description: schema.string(),
|
||||
description: schema.nullable(schema.string()),
|
||||
externalId: schema.nullable(schema.string()),
|
||||
};
|
||||
|
||||
// Schema for ServiceNow Incident Management (ITSM)
|
||||
export const ExecutorSubActionPushParamsSchemaITSM = schema.object({
|
||||
incident: schema.object({
|
||||
short_description: schema.string(),
|
||||
description: schema.nullable(schema.string()),
|
||||
externalId: schema.nullable(schema.string()),
|
||||
...CommonAttributes,
|
||||
severity: schema.nullable(schema.string()),
|
||||
urgency: schema.nullable(schema.string()),
|
||||
impact: schema.nullable(schema.string()),
|
||||
}),
|
||||
comments: schema.nullable(
|
||||
schema.arrayOf(
|
||||
schema.object({
|
||||
comment: schema.string(),
|
||||
commentId: schema.string(),
|
||||
})
|
||||
)
|
||||
),
|
||||
comments: CommentsSchema,
|
||||
});
|
||||
|
||||
// Schema for ServiceNow Security Incident Response (SIR)
|
||||
export const ExecutorSubActionPushParamsSchemaSIR = schema.object({
|
||||
incident: schema.object({
|
||||
...CommonAttributes,
|
||||
category: schema.nullable(schema.string()),
|
||||
dest_ip: schema.nullable(schema.string()),
|
||||
malware_hash: schema.nullable(schema.string()),
|
||||
malware_url: schema.nullable(schema.string()),
|
||||
priority: schema.nullable(schema.string()),
|
||||
source_ip: schema.nullable(schema.string()),
|
||||
subcategory: schema.nullable(schema.string()),
|
||||
}),
|
||||
comments: CommentsSchema,
|
||||
});
|
||||
|
||||
export const ExecutorSubActionGetIncidentParamsSchema = schema.object({
|
||||
|
@ -56,8 +79,12 @@ export const ExecutorSubActionGetIncidentParamsSchema = schema.object({
|
|||
// Reserved for future implementation
|
||||
export const ExecutorSubActionHandshakeParamsSchema = schema.object({});
|
||||
export const ExecutorSubActionCommonFieldsParamsSchema = schema.object({});
|
||||
export const ExecutorSubActionGetChoicesParamsSchema = schema.object({
|
||||
fields: schema.arrayOf(schema.string()),
|
||||
});
|
||||
|
||||
export const ExecutorParamsSchema = schema.oneOf([
|
||||
// Executor parameters for ServiceNow Incident Management (ITSM)
|
||||
export const ExecutorParamsSchemaITSM = schema.oneOf([
|
||||
schema.object({
|
||||
subAction: schema.literal('getFields'),
|
||||
subActionParams: ExecutorSubActionCommonFieldsParamsSchema,
|
||||
|
@ -72,6 +99,34 @@ export const ExecutorParamsSchema = schema.oneOf([
|
|||
}),
|
||||
schema.object({
|
||||
subAction: schema.literal('pushToService'),
|
||||
subActionParams: ExecutorSubActionPushParamsSchema,
|
||||
subActionParams: ExecutorSubActionPushParamsSchemaITSM,
|
||||
}),
|
||||
schema.object({
|
||||
subAction: schema.literal('getChoices'),
|
||||
subActionParams: ExecutorSubActionGetChoicesParamsSchema,
|
||||
}),
|
||||
]);
|
||||
|
||||
// Executor parameters for ServiceNow Security Incident Response (SIR)
|
||||
export const ExecutorParamsSchemaSIR = schema.oneOf([
|
||||
schema.object({
|
||||
subAction: schema.literal('getFields'),
|
||||
subActionParams: ExecutorSubActionCommonFieldsParamsSchema,
|
||||
}),
|
||||
schema.object({
|
||||
subAction: schema.literal('getIncident'),
|
||||
subActionParams: ExecutorSubActionGetIncidentParamsSchema,
|
||||
}),
|
||||
schema.object({
|
||||
subAction: schema.literal('handshake'),
|
||||
subActionParams: ExecutorSubActionHandshakeParamsSchema,
|
||||
}),
|
||||
schema.object({
|
||||
subAction: schema.literal('pushToService'),
|
||||
subActionParams: ExecutorSubActionPushParamsSchemaSIR,
|
||||
}),
|
||||
schema.object({
|
||||
subAction: schema.literal('getChoices'),
|
||||
subActionParams: ExecutorSubActionGetChoicesParamsSchema,
|
||||
}),
|
||||
]);
|
||||
|
|
|
@ -12,7 +12,7 @@ import { ExternalService } from './types';
|
|||
import { Logger } from '../../../../../../src/core/server';
|
||||
import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
|
||||
import { actionsConfigMock } from '../../actions_config.mock';
|
||||
import { serviceNowCommonFields } from './mocks';
|
||||
import { serviceNowCommonFields, serviceNowChoices } from './mocks';
|
||||
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
|
||||
jest.mock('axios');
|
||||
|
@ -29,12 +29,14 @@ axios.create = jest.fn(() => axios);
|
|||
const requestMock = utils.request as jest.Mock;
|
||||
const patchMock = utils.patch as jest.Mock;
|
||||
const configurationUtilities = actionsConfigMock.create();
|
||||
const table = 'incident';
|
||||
|
||||
describe('ServiceNow service', () => {
|
||||
let service: ExternalService;
|
||||
|
||||
beforeAll(() => {
|
||||
beforeEach(() => {
|
||||
service = createExternalService(
|
||||
table,
|
||||
{
|
||||
// The trailing slash at the end of the url is intended.
|
||||
// All API calls need to have the trailing slash removed.
|
||||
|
@ -54,6 +56,7 @@ describe('ServiceNow service', () => {
|
|||
test('throws without url', () => {
|
||||
expect(() =>
|
||||
createExternalService(
|
||||
table,
|
||||
{
|
||||
config: { apiUrl: null },
|
||||
secrets: { username: 'admin', password: 'admin' },
|
||||
|
@ -67,6 +70,7 @@ describe('ServiceNow service', () => {
|
|||
test('throws without username', () => {
|
||||
expect(() =>
|
||||
createExternalService(
|
||||
table,
|
||||
{
|
||||
config: { apiUrl: 'test.com' },
|
||||
secrets: { username: '', password: 'admin' },
|
||||
|
@ -80,6 +84,7 @@ describe('ServiceNow service', () => {
|
|||
test('throws without password', () => {
|
||||
expect(() =>
|
||||
createExternalService(
|
||||
table,
|
||||
{
|
||||
config: { apiUrl: 'test.com' },
|
||||
secrets: { username: '', password: undefined },
|
||||
|
@ -114,6 +119,30 @@ describe('ServiceNow service', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('it should call request with correct arguments when table changes', async () => {
|
||||
service = createExternalService(
|
||||
'sn_si_incident',
|
||||
{
|
||||
config: { apiUrl: 'https://dev102283.service-now.com/' },
|
||||
secrets: { username: 'admin', password: 'admin' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
);
|
||||
|
||||
requestMock.mockImplementation(() => ({
|
||||
data: { result: { sys_id: '1', number: 'INC01' } },
|
||||
}));
|
||||
|
||||
await service.getIncident('1');
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
axios,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
url: 'https://dev102283.service-now.com/api/now/v2/table/sn_si_incident/1',
|
||||
});
|
||||
});
|
||||
|
||||
test('it should throw an error', async () => {
|
||||
requestMock.mockImplementation(() => {
|
||||
throw new Error('An error has occurred');
|
||||
|
@ -122,6 +151,17 @@ describe('ServiceNow service', () => {
|
|||
'Unable to get incident with id 1. Error: An error has occurred'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should throw an error when instance is not alive', async () => {
|
||||
requestMock.mockImplementation(() => ({
|
||||
status: 200,
|
||||
data: {},
|
||||
request: { connection: { servername: 'Developer instance' } },
|
||||
}));
|
||||
await expect(service.getIncident('1')).rejects.toThrow(
|
||||
'There is an issue with your Service Now Instance. Please check Developer instance.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createIncident', () => {
|
||||
|
@ -161,6 +201,39 @@ describe('ServiceNow service', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('it should call request with correct arguments when table changes', async () => {
|
||||
service = createExternalService(
|
||||
'sn_si_incident',
|
||||
{
|
||||
config: { apiUrl: 'https://dev102283.service-now.com/' },
|
||||
secrets: { username: 'admin', password: 'admin' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
);
|
||||
|
||||
requestMock.mockImplementation(() => ({
|
||||
data: { result: { sys_id: '1', number: 'INC01', sys_created_on: '2020-03-10 12:24:20' } },
|
||||
}));
|
||||
|
||||
const res = await service.createIncident({
|
||||
incident: { short_description: 'title', description: 'desc' },
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
axios,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
url: 'https://dev102283.service-now.com/api/now/v2/table/sn_si_incident',
|
||||
method: 'post',
|
||||
data: { short_description: 'title', description: 'desc' },
|
||||
});
|
||||
|
||||
expect(res.url).toEqual(
|
||||
'https://dev102283.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=1'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should throw an error', async () => {
|
||||
requestMock.mockImplementation(() => {
|
||||
throw new Error('An error has occurred');
|
||||
|
@ -174,6 +247,17 @@ describe('ServiceNow service', () => {
|
|||
'[Action][ServiceNow]: Unable to create incident. Error: An error has occurred'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should throw an error when instance is not alive', async () => {
|
||||
requestMock.mockImplementation(() => ({
|
||||
status: 200,
|
||||
data: {},
|
||||
request: { connection: { servername: 'Developer instance' } },
|
||||
}));
|
||||
await expect(service.getIncident('1')).rejects.toThrow(
|
||||
'There is an issue with your Service Now Instance. Please check Developer instance.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateIncident', () => {
|
||||
|
@ -214,6 +298,39 @@ describe('ServiceNow service', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('it should call request with correct arguments when table changes', async () => {
|
||||
service = createExternalService(
|
||||
'sn_si_incident',
|
||||
{
|
||||
config: { apiUrl: 'https://dev102283.service-now.com/' },
|
||||
secrets: { username: 'admin', password: 'admin' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
);
|
||||
|
||||
patchMock.mockImplementation(() => ({
|
||||
data: { result: { sys_id: '1', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' } },
|
||||
}));
|
||||
|
||||
const res = await service.updateIncident({
|
||||
incidentId: '1',
|
||||
incident: { short_description: 'title', description: 'desc' },
|
||||
});
|
||||
|
||||
expect(patchMock).toHaveBeenCalledWith({
|
||||
axios,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
url: 'https://dev102283.service-now.com/api/now/v2/table/sn_si_incident/1',
|
||||
data: { short_description: 'title', description: 'desc' },
|
||||
});
|
||||
|
||||
expect(res.url).toEqual(
|
||||
'https://dev102283.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=1'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should throw an error', async () => {
|
||||
patchMock.mockImplementation(() => {
|
||||
throw new Error('An error has occurred');
|
||||
|
@ -228,6 +345,7 @@ describe('ServiceNow service', () => {
|
|||
'[Action][ServiceNow]: Unable to update incident with id 1. Error: An error has occurred'
|
||||
);
|
||||
});
|
||||
|
||||
test('it creates the comment correctly', async () => {
|
||||
patchMock.mockImplementation(() => ({
|
||||
data: { result: { sys_id: '11', number: 'INC011', sys_updated_on: '2020-03-10 12:24:20' } },
|
||||
|
@ -245,6 +363,17 @@ describe('ServiceNow service', () => {
|
|||
url: 'https://dev102283.service-now.com/nav_to.do?uri=incident.do?sys_id=11',
|
||||
});
|
||||
});
|
||||
|
||||
test('it should throw an error when instance is not alive', async () => {
|
||||
requestMock.mockImplementation(() => ({
|
||||
status: 200,
|
||||
data: {},
|
||||
request: { connection: { servername: 'Developer instance' } },
|
||||
}));
|
||||
await expect(service.getIncident('1')).rejects.toThrow(
|
||||
'There is an issue with your Service Now Instance. Please check Developer instance.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFields', () => {
|
||||
|
@ -259,9 +388,10 @@ describe('ServiceNow service', () => {
|
|||
logger,
|
||||
configurationUtilities,
|
||||
url:
|
||||
'https://dev102283.service-now.com/api/now/v2/table/sys_dictionary?sysparm_query=name=task^internal_type=string&active=true&array=false&read_only=false&sysparm_fields=max_length,element,column_label,mandatory',
|
||||
'https://dev102283.service-now.com/api/now/v2/table/sys_dictionary?sysparm_query=name=task^ORname=incident^internal_type=string&active=true&array=false&read_only=false&sysparm_fields=max_length,element,column_label,mandatory',
|
||||
});
|
||||
});
|
||||
|
||||
test('it returns common fields correctly', async () => {
|
||||
requestMock.mockImplementation(() => ({
|
||||
data: { result: serviceNowCommonFields },
|
||||
|
@ -270,6 +400,31 @@ describe('ServiceNow service', () => {
|
|||
expect(res).toEqual(serviceNowCommonFields);
|
||||
});
|
||||
|
||||
test('it should call request with correct arguments when table changes', async () => {
|
||||
service = createExternalService(
|
||||
'sn_si_incident',
|
||||
{
|
||||
config: { apiUrl: 'https://dev102283.service-now.com/' },
|
||||
secrets: { username: 'admin', password: 'admin' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
);
|
||||
|
||||
requestMock.mockImplementation(() => ({
|
||||
data: { result: serviceNowCommonFields },
|
||||
}));
|
||||
await service.getFields();
|
||||
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
axios,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
url:
|
||||
'https://dev102283.service-now.com/api/now/v2/table/sys_dictionary?sysparm_query=name=task^ORname=sn_si_incident^internal_type=string&active=true&array=false&read_only=false&sysparm_fields=max_length,element,column_label,mandatory',
|
||||
});
|
||||
});
|
||||
|
||||
test('it should throw an error', async () => {
|
||||
requestMock.mockImplementation(() => {
|
||||
throw new Error('An error has occurred');
|
||||
|
@ -278,5 +433,87 @@ describe('ServiceNow service', () => {
|
|||
'[Action][ServiceNow]: Unable to get fields. Error: An error has occurred'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should throw an error when instance is not alive', async () => {
|
||||
requestMock.mockImplementation(() => ({
|
||||
status: 200,
|
||||
data: {},
|
||||
request: { connection: { servername: 'Developer instance' } },
|
||||
}));
|
||||
await expect(service.getIncident('1')).rejects.toThrow(
|
||||
'There is an issue with your Service Now Instance. Please check Developer instance.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getChoices', () => {
|
||||
test('it should call request with correct arguments', async () => {
|
||||
requestMock.mockImplementation(() => ({
|
||||
data: { result: serviceNowChoices },
|
||||
}));
|
||||
await service.getChoices(['priority', 'category']);
|
||||
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
axios,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
url:
|
||||
'https://dev102283.service-now.com/api/now/v2/table/sys_choice?sysparm_query=name=task^ORname=incident^element=priority^ORelement=category&sysparm_fields=label,value,dependent_value,element',
|
||||
});
|
||||
});
|
||||
|
||||
test('it returns common fields correctly', async () => {
|
||||
requestMock.mockImplementation(() => ({
|
||||
data: { result: serviceNowChoices },
|
||||
}));
|
||||
const res = await service.getChoices(['priority']);
|
||||
expect(res).toEqual(serviceNowChoices);
|
||||
});
|
||||
|
||||
test('it should call request with correct arguments when table changes', async () => {
|
||||
service = createExternalService(
|
||||
'sn_si_incident',
|
||||
{
|
||||
config: { apiUrl: 'https://dev102283.service-now.com/' },
|
||||
secrets: { username: 'admin', password: 'admin' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
);
|
||||
|
||||
requestMock.mockImplementation(() => ({
|
||||
data: { result: serviceNowChoices },
|
||||
}));
|
||||
|
||||
await service.getChoices(['priority', 'category']);
|
||||
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
axios,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
url:
|
||||
'https://dev102283.service-now.com/api/now/v2/table/sys_choice?sysparm_query=name=task^ORname=sn_si_incident^element=priority^ORelement=category&sysparm_fields=label,value,dependent_value,element',
|
||||
});
|
||||
});
|
||||
|
||||
test('it should throw an error', async () => {
|
||||
requestMock.mockImplementation(() => {
|
||||
throw new Error('An error has occurred');
|
||||
});
|
||||
await expect(service.getChoices(['priority'])).rejects.toThrow(
|
||||
'[Action][ServiceNow]: Unable to get choices. Error: An error has occurred'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should throw an error when instance is not alive', async () => {
|
||||
requestMock.mockImplementation(() => ({
|
||||
status: 200,
|
||||
data: {},
|
||||
request: { connection: { servername: 'Developer instance' } },
|
||||
}));
|
||||
await expect(service.getIncident('1')).rejects.toThrow(
|
||||
'There is an issue with your Service Now Instance. Please check Developer instance.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,13 +15,10 @@ import { request, getErrorMessage, addTimeZoneToDate, patch } from '../lib/axios
|
|||
import { ActionsConfigurationUtilities } from '../../actions_config';
|
||||
|
||||
const API_VERSION = 'v2';
|
||||
const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`;
|
||||
const SYS_DICTIONARY = `api/now/${API_VERSION}/table/sys_dictionary`;
|
||||
|
||||
// Based on: https://docs.servicenow.com/bundle/orlando-platform-user-interface/page/use/navigation/reference/r_NavigatingByURLExamples.html
|
||||
const VIEW_INCIDENT_URL = `nav_to.do?uri=incident.do?sys_id=`;
|
||||
|
||||
export const createExternalService = (
|
||||
table: string,
|
||||
{ config, secrets }: ExternalServiceCredentials,
|
||||
logger: Logger,
|
||||
configurationUtilities: ActionsConfigurationUtilities
|
||||
|
@ -30,24 +27,36 @@ export const createExternalService = (
|
|||
const { username, password } = secrets as ServiceNowSecretConfigurationType;
|
||||
|
||||
if (!url || !username || !password) {
|
||||
throw Error(`[Action]${i18n.NAME}: Wrong configuration.`);
|
||||
throw Error(`[Action]${i18n.SERVICENOW}: Wrong configuration.`);
|
||||
}
|
||||
|
||||
const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url;
|
||||
const incidentUrl = `${urlWithoutTrailingSlash}/${INCIDENT_URL}`;
|
||||
const fieldsUrl = `${urlWithoutTrailingSlash}/${SYS_DICTIONARY}?sysparm_query=name=task^internal_type=string&active=true&array=false&read_only=false&sysparm_fields=max_length,element,column_label,mandatory`;
|
||||
const incidentUrl = `${urlWithoutTrailingSlash}/api/now/${API_VERSION}/table/${table}`;
|
||||
const fieldsUrl = `${urlWithoutTrailingSlash}/${SYS_DICTIONARY}?sysparm_query=name=task^ORname=${table}^internal_type=string&active=true&array=false&read_only=false&sysparm_fields=max_length,element,column_label,mandatory`;
|
||||
const choicesUrl = `${urlWithoutTrailingSlash}/api/now/${API_VERSION}/table/sys_choice`;
|
||||
const axiosInstance = axios.create({
|
||||
auth: { username, password },
|
||||
});
|
||||
|
||||
const getIncidentViewURL = (id: string) => {
|
||||
return `${urlWithoutTrailingSlash}/${VIEW_INCIDENT_URL}${id}`;
|
||||
// Based on: https://docs.servicenow.com/bundle/orlando-platform-user-interface/page/use/navigation/reference/r_NavigatingByURLExamples.html
|
||||
return `${urlWithoutTrailingSlash}/nav_to.do?uri=${table}.do?sys_id=${id}`;
|
||||
};
|
||||
|
||||
const getChoicesURL = (fields: string[]) => {
|
||||
const elements = fields
|
||||
.slice(1)
|
||||
.reduce((acc, field) => `${acc}^ORelement=${field}`, `element=${fields[0]}`);
|
||||
|
||||
return `${choicesUrl}?sysparm_query=name=task^ORname=${table}^${elements}&sysparm_fields=label,value,dependent_value,element`;
|
||||
};
|
||||
|
||||
const checkInstance = (res: AxiosResponse) => {
|
||||
if (res.status === 200 && res.data.result == null) {
|
||||
throw new Error(
|
||||
`There is an issue with your Service Now Instance. Please check ${res.request.connection.servername}`
|
||||
`There is an issue with your Service Now Instance. Please check ${
|
||||
res.request?.connection?.servername ?? ''
|
||||
}.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -64,7 +73,10 @@ export const createExternalService = (
|
|||
return { ...res.data.result };
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
getErrorMessage(i18n.NAME, `Unable to get incident with id ${id}. Error: ${error.message}`)
|
||||
getErrorMessage(
|
||||
i18n.SERVICENOW,
|
||||
`Unable to get incident with id ${id}. Error: ${error.message}`
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -82,7 +94,10 @@ export const createExternalService = (
|
|||
return res.data.result.length > 0 ? { ...res.data.result } : undefined;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
getErrorMessage(i18n.NAME, `Unable to find incidents by query. Error: ${error.message}`)
|
||||
getErrorMessage(
|
||||
i18n.SERVICENOW,
|
||||
`Unable to find incidents by query. Error: ${error.message}`
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -106,7 +121,7 @@ export const createExternalService = (
|
|||
};
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
getErrorMessage(i18n.NAME, `Unable to create incident. Error: ${error.message}`)
|
||||
getErrorMessage(i18n.SERVICENOW, `Unable to create incident. Error: ${error.message}`)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -130,7 +145,7 @@ export const createExternalService = (
|
|||
} catch (error) {
|
||||
throw new Error(
|
||||
getErrorMessage(
|
||||
i18n.NAME,
|
||||
i18n.SERVICENOW,
|
||||
`Unable to update incident with id ${incidentId}. Error: ${error.message}`
|
||||
)
|
||||
);
|
||||
|
@ -148,7 +163,26 @@ export const createExternalService = (
|
|||
checkInstance(res);
|
||||
return res.data.result.length > 0 ? res.data.result : [];
|
||||
} catch (error) {
|
||||
throw new Error(getErrorMessage(i18n.NAME, `Unable to get fields. Error: ${error.message}`));
|
||||
throw new Error(
|
||||
getErrorMessage(i18n.SERVICENOW, `Unable to get fields. Error: ${error.message}`)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getChoices = async (fields: string[]) => {
|
||||
try {
|
||||
const res = await request({
|
||||
axios: axiosInstance,
|
||||
url: getChoicesURL(fields),
|
||||
logger,
|
||||
configurationUtilities,
|
||||
});
|
||||
checkInstance(res);
|
||||
return res.data.result;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
getErrorMessage(i18n.SERVICENOW, `Unable to get choices. Error: ${error.message}`)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -158,5 +192,6 @@ export const createExternalService = (
|
|||
getFields,
|
||||
getIncident,
|
||||
updateIncident,
|
||||
getChoices,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -6,10 +6,18 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const NAME = i18n.translate('xpack.actions.builtin.servicenowTitle', {
|
||||
export const SERVICENOW = i18n.translate('xpack.actions.builtin.serviceNowTitle', {
|
||||
defaultMessage: 'ServiceNow',
|
||||
});
|
||||
|
||||
export const SERVICENOW_ITSM = i18n.translate('xpack.actions.builtin.serviceNowITSMTitle', {
|
||||
defaultMessage: 'ServiceNow ITSM',
|
||||
});
|
||||
|
||||
export const SERVICENOW_SIR = i18n.translate('xpack.actions.builtin.serviceNowSIRTitle', {
|
||||
defaultMessage: 'ServiceNow SIR',
|
||||
});
|
||||
|
||||
export const ALLOWED_HOSTS_ERROR = (message: string) =>
|
||||
i18n.translate('xpack.actions.builtin.configuration.apiAllowedHostsError', {
|
||||
defaultMessage: 'error configuring connector action: {message}',
|
||||
|
|
|
@ -8,13 +8,16 @@
|
|||
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import {
|
||||
ExecutorParamsSchema,
|
||||
ExecutorParamsSchemaITSM,
|
||||
ExecutorSubActionCommonFieldsParamsSchema,
|
||||
ExecutorSubActionGetIncidentParamsSchema,
|
||||
ExecutorSubActionHandshakeParamsSchema,
|
||||
ExecutorSubActionPushParamsSchema,
|
||||
ExecutorSubActionPushParamsSchemaITSM,
|
||||
ExternalIncidentServiceConfigurationSchema,
|
||||
ExternalIncidentServiceSecretConfigurationSchema,
|
||||
ExecutorParamsSchemaSIR,
|
||||
ExecutorSubActionPushParamsSchemaSIR,
|
||||
ExecutorSubActionGetChoicesParamsSchema,
|
||||
} from './schema';
|
||||
import { ActionsConfigurationUtilities } from '../../actions_config';
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
|
@ -30,14 +33,29 @@ export type ExecutorSubActionCommonFieldsParams = TypeOf<
|
|||
typeof ExecutorSubActionCommonFieldsParamsSchema
|
||||
>;
|
||||
|
||||
export type ServiceNowExecutorResultData = PushToServiceResponse | GetCommonFieldsResponse;
|
||||
export type ExecutorSubActionGetChoicesParams = TypeOf<
|
||||
typeof ExecutorSubActionGetChoicesParamsSchema
|
||||
>;
|
||||
|
||||
export type ServiceNowExecutorResultData =
|
||||
| PushToServiceResponse
|
||||
| GetCommonFieldsResponse
|
||||
| GetChoicesResponse;
|
||||
|
||||
export interface CreateCommentRequest {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export type ExecutorParams = TypeOf<typeof ExecutorParamsSchema>;
|
||||
export type ExecutorSubActionPushParams = TypeOf<typeof ExecutorSubActionPushParamsSchema>;
|
||||
export type ExecutorParams =
|
||||
| TypeOf<typeof ExecutorParamsSchemaITSM>
|
||||
| TypeOf<typeof ExecutorParamsSchemaSIR>;
|
||||
|
||||
export type ExecutorSubActionPushParamsITSM = TypeOf<typeof ExecutorSubActionPushParamsSchemaITSM>;
|
||||
export type ExecutorSubActionPushParamsSIR = TypeOf<typeof ExecutorSubActionPushParamsSchemaSIR>;
|
||||
|
||||
export type ExecutorSubActionPushParams =
|
||||
| ExecutorSubActionPushParamsITSM
|
||||
| ExecutorSubActionPushParamsSIR;
|
||||
|
||||
export interface ExternalServiceCredentials {
|
||||
config: Record<string, unknown>;
|
||||
|
@ -62,14 +80,17 @@ export interface PushToServiceResponse extends ExternalServiceIncidentResponse {
|
|||
export type ExternalServiceParams = Record<string, unknown>;
|
||||
|
||||
export interface ExternalService {
|
||||
getFields: () => Promise<GetCommonFieldsResponse>;
|
||||
getChoices: (fields: string[]) => Promise<GetChoicesResponse>;
|
||||
getIncident: (id: string) => Promise<ExternalServiceParams | undefined>;
|
||||
getFields: () => Promise<GetCommonFieldsResponse>;
|
||||
createIncident: (params: ExternalServiceParams) => Promise<ExternalServiceIncidentResponse>;
|
||||
updateIncident: (params: ExternalServiceParams) => Promise<ExternalServiceIncidentResponse>;
|
||||
findIncidents: (params?: Record<string, string>) => Promise<ExternalServiceParams[] | undefined>;
|
||||
}
|
||||
|
||||
export type PushToServiceApiParams = ExecutorSubActionPushParams;
|
||||
export type PushToServiceApiParamsITSM = ExecutorSubActionPushParamsITSM;
|
||||
export type PushToServiceApiParamsSIR = ExecutorSubActionPushParamsSIR;
|
||||
|
||||
export interface ExternalServiceApiHandlerArgs {
|
||||
externalService: ExternalService;
|
||||
|
@ -83,7 +104,17 @@ export type ExecutorSubActionHandshakeParams = TypeOf<
|
|||
typeof ExecutorSubActionHandshakeParamsSchema
|
||||
>;
|
||||
|
||||
export type Incident = Omit<ExecutorSubActionPushParams['incident'], 'externalId'>;
|
||||
export type ServiceNowITSMIncident = Omit<
|
||||
TypeOf<typeof ExecutorSubActionPushParamsSchemaITSM>['incident'],
|
||||
'externalId'
|
||||
>;
|
||||
|
||||
export type ServiceNowSIRIncident = Omit<
|
||||
TypeOf<typeof ExecutorSubActionPushParamsSchemaSIR>['incident'],
|
||||
'externalId'
|
||||
>;
|
||||
|
||||
export type Incident = ServiceNowITSMIncident | ServiceNowSIRIncident;
|
||||
|
||||
export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs {
|
||||
params: PushToServiceApiParams;
|
||||
|
@ -104,13 +135,29 @@ export interface ExternalServiceFields {
|
|||
max_length: string;
|
||||
element: string;
|
||||
}
|
||||
|
||||
export interface ExternalServiceChoices {
|
||||
value: string;
|
||||
label: string;
|
||||
dependent_value: string;
|
||||
element: string;
|
||||
}
|
||||
|
||||
export type GetCommonFieldsResponse = ExternalServiceFields[];
|
||||
export type GetChoicesResponse = ExternalServiceChoices[];
|
||||
|
||||
export interface GetCommonFieldsHandlerArgs {
|
||||
externalService: ExternalService;
|
||||
params: ExecutorSubActionCommonFieldsParams;
|
||||
}
|
||||
|
||||
export interface GetChoicesHandlerArgs {
|
||||
externalService: ExternalService;
|
||||
params: ExecutorSubActionGetChoicesParams;
|
||||
}
|
||||
|
||||
export interface ExternalServiceApi {
|
||||
getChoices: (args: GetChoicesHandlerArgs) => Promise<GetChoicesResponse>;
|
||||
getFields: (args: GetCommonFieldsHandlerArgs) => Promise<GetCommonFieldsResponse>;
|
||||
handshake: (args: HandshakeApiHandlerArgs) => Promise<void>;
|
||||
pushToService: (args: PushToServiceApiHandlerArgs) => Promise<PushToServiceResponse>;
|
||||
|
|
|
@ -35,7 +35,8 @@ export type {
|
|||
SlackActionParams,
|
||||
WebhookActionTypeId,
|
||||
WebhookActionParams,
|
||||
ServiceNowActionTypeId,
|
||||
ServiceNowITSMActionTypeId,
|
||||
ServiceNowSIRActionTypeId,
|
||||
ServiceNowActionParams,
|
||||
JiraActionTypeId,
|
||||
JiraActionParams,
|
||||
|
|
|
@ -16,8 +16,8 @@ import {
|
|||
Incident as ResilientIncident,
|
||||
} from '../../../../actions/server/builtin_action_types/resilient/types';
|
||||
import {
|
||||
PushToServiceApiParams as ServiceNowPushToServiceApiParams,
|
||||
Incident as ServiceNowIncident,
|
||||
PushToServiceApiParamsITSM as ServiceNowITSMPushToServiceApiParams,
|
||||
ServiceNowITSMIncident,
|
||||
} from '../../../../actions/server/builtin_action_types/servicenow/types';
|
||||
import { ResilientFieldsRT } from './resilient';
|
||||
import { ServiceNowFieldsRT } from './servicenow';
|
||||
|
@ -33,13 +33,13 @@ export interface ElasticUser {
|
|||
export {
|
||||
JiraPushToServiceApiParams,
|
||||
ResilientPushToServiceApiParams,
|
||||
ServiceNowPushToServiceApiParams,
|
||||
ServiceNowITSMPushToServiceApiParams,
|
||||
};
|
||||
export type Incident = JiraIncident | ResilientIncident | ServiceNowIncident;
|
||||
export type Incident = JiraIncident | ResilientIncident | ServiceNowITSMIncident;
|
||||
export type PushToServiceApiParams =
|
||||
| JiraPushToServiceApiParams
|
||||
| ResilientPushToServiceApiParams
|
||||
| ServiceNowPushToServiceApiParams;
|
||||
| ServiceNowITSMPushToServiceApiParams;
|
||||
|
||||
const ActionTypeRT = rt.union([
|
||||
rt.literal('append'),
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
PrepareFieldsForTransformArgs,
|
||||
PushToServiceApiParams,
|
||||
ResilientPushToServiceApiParams,
|
||||
ServiceNowPushToServiceApiParams,
|
||||
ServiceNowITSMPushToServiceApiParams,
|
||||
SimpleComment,
|
||||
Transformer,
|
||||
TransformerArgs,
|
||||
|
@ -105,7 +105,11 @@ export const serviceFormatter = (
|
|||
thirdPartyName: 'Resilient',
|
||||
};
|
||||
case ConnectorTypes.servicenow:
|
||||
const { severity, urgency, impact } = params as ServiceNowPushToServiceApiParams['incident'];
|
||||
const {
|
||||
severity,
|
||||
urgency,
|
||||
impact,
|
||||
} = params as ServiceNowITSMPushToServiceApiParams['incident'];
|
||||
return {
|
||||
incident: { severity, urgency, impact },
|
||||
thirdPartyName: 'ServiceNow',
|
||||
|
|
|
@ -40,7 +40,7 @@ describe('Mapping', () => {
|
|||
wrappingComponent: TestProviders,
|
||||
});
|
||||
expect(wrapper.find('[data-test-subj="field-mapping-desc"]').first().text()).toBe(
|
||||
'Field mappings require an established connection to ServiceNow. Please check your connection credentials.'
|
||||
'Field mappings require an established connection to ServiceNow ITSM. Please check your connection credentials.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,14 +7,14 @@
|
|||
/* eslint-disable @kbn/eslint/no-restricted-paths */
|
||||
|
||||
import {
|
||||
ServiceNowConnectorConfiguration,
|
||||
ServiceNowITSMConnectorConfiguration,
|
||||
JiraConnectorConfiguration,
|
||||
ResilientConnectorConfiguration,
|
||||
} from '../../../../../triggers_actions_ui/public/common';
|
||||
import { ConnectorConfiguration } from './types';
|
||||
|
||||
export const connectorsConfiguration: Record<string, ConnectorConfiguration> = {
|
||||
'.servicenow': ServiceNowConnectorConfiguration as ConnectorConfiguration,
|
||||
'.servicenow': ServiceNowITSMConnectorConfiguration as ConnectorConfiguration,
|
||||
'.jira': JiraConnectorConfiguration as ConnectorConfiguration,
|
||||
'.resilient': ResilientConnectorConfiguration as ConnectorConfiguration,
|
||||
};
|
||||
|
|
|
@ -22,5 +22,4 @@ export interface ThirdPartyField {
|
|||
|
||||
export interface ConnectorConfiguration extends ActionType {
|
||||
logo: string;
|
||||
fields: Record<string, ThirdPartyField>;
|
||||
}
|
||||
|
|
|
@ -4784,7 +4784,6 @@
|
|||
"xpack.actions.builtin.pagerdutyTitle": "PagerDuty",
|
||||
"xpack.actions.builtin.serverLog.errorLoggingErrorMessage": "メッセージのロギングエラー",
|
||||
"xpack.actions.builtin.serverLogTitle": "サーバーログ",
|
||||
"xpack.actions.builtin.servicenowTitle": "ServiceNow",
|
||||
"xpack.actions.builtin.slack.errorPostingErrorMessage": "slack メッセージの投稿エラー",
|
||||
"xpack.actions.builtin.slack.errorPostingRetryDateErrorMessage": "slack メッセージの投稿エラー、 {retryString} で再試行",
|
||||
"xpack.actions.builtin.slack.errorPostingRetryLaterErrorMessage": "slack メッセージの投稿エラー、後ほど再試行",
|
||||
|
@ -21161,7 +21160,6 @@
|
|||
"xpack.triggersActionsUI.common.expressionItems.threshold.popoverTitle": "タイミング",
|
||||
"xpack.triggersActionsUI.components.addMessageVariables.addVariablePopoverButton": "変数を追加",
|
||||
"xpack.triggersActionsUI.components.addMessageVariables.addVariableTitle": "アラート変数を追加",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.common.requiredDescriptionTextField": "説明が必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.common.requiredShortDescTextField": "短い説明が必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.emailAction.actionTypeTitle": "メールに送信",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.emailAction.configureAccountsHelpLabel": "電子メールアカウントの構成",
|
||||
|
@ -21292,35 +21290,16 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.logLevelFieldLabel": "レベル",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.logMessageFieldLabel": "メッセージ",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.selectMessageText": "Kibana ログにメッセージを追加します。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.actionTypeTitle": "ServiceNow",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiTokenTextFieldLabel": "APIトークン",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiUrlTextFieldLabel": "URL",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.authenticationLabel": "認証",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.commentsTextAreaFieldLabel": "追加のコメント",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.descriptionTextAreaFieldLabel": "説明",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.emailTextFieldLabel": "メール",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.impactSelectFieldLabel": "インパクト",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.invalidApiUrlTextField": "URL が無効です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.mappingFieldComments": "コメント",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.mappingFieldDescription": "説明",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.mappingFieldShortDescription": "短い説明",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.passwordTextFieldLabel": "パスワード",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.reenterValuesLabel": "ユーザー名とパスワードは暗号化されます。これらのフィールドの値を再入力してください。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.rememberValuesLabel": "これらの値を覚えておいてください。コネクターを編集するたびに再入力する必要があります。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredApiTokenTextField": "API トークンが必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredApiUrlTextField": "URL が必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredEmailTextField": "電子メールが必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredPasswordTextField": "パスワードが必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredUsernameTextField": "ユーザー名が必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requireHttpsApiUrlTextField": "URL は https:// から始める必要があります。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.selectMessageText": "ServiceNow でインシデントを作成します。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.severitySelectFieldLabel": "深刻度",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectHighOptionLabel": "高",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectLawOptionLabel": "低",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectMediumOptionLabel": "中",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.title": "インシデント",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.titleFieldLabel": "短い説明(必須)",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.urgencySelectFieldLabel": "緊急",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.usernameTextFieldLabel": "ユーザー名",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.serviceNowAction.apiUrlHelpLabel": "Personal Developer Instance の構成",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.actionTypeTitle": "Slack に送信",
|
||||
|
|
|
@ -4789,7 +4789,6 @@
|
|||
"xpack.actions.builtin.pagerdutyTitle": "PagerDuty",
|
||||
"xpack.actions.builtin.serverLog.errorLoggingErrorMessage": "记录消息时出错",
|
||||
"xpack.actions.builtin.serverLogTitle": "服务器日志",
|
||||
"xpack.actions.builtin.servicenowTitle": "ServiceNow",
|
||||
"xpack.actions.builtin.slack.errorPostingErrorMessage": "发布 slack 消息时出错",
|
||||
"xpack.actions.builtin.slack.errorPostingRetryDateErrorMessage": "发布 Slack 消息时出错,在 {retryString} 重试",
|
||||
"xpack.actions.builtin.slack.errorPostingRetryLaterErrorMessage": "发布 slack 消息时出错,稍后重试",
|
||||
|
@ -21212,7 +21211,6 @@
|
|||
"xpack.triggersActionsUI.common.expressionItems.threshold.popoverTitle": "当",
|
||||
"xpack.triggersActionsUI.components.addMessageVariables.addVariablePopoverButton": "添加变量",
|
||||
"xpack.triggersActionsUI.components.addMessageVariables.addVariableTitle": "添加告警变量",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.common.requiredDescriptionTextField": "“描述”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.common.requiredShortDescTextField": "“简短描述”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.emailAction.actionTypeTitle": "发送到电子邮件",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.emailAction.configureAccountsHelpLabel": "配置电子邮件帐户",
|
||||
|
@ -21343,35 +21341,16 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.logLevelFieldLabel": "级别",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.logMessageFieldLabel": "消息",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.selectMessageText": "将消息添加到 Kibana 日志。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.actionTypeTitle": "ServiceNow",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiTokenTextFieldLabel": "Api 令牌",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiUrlTextFieldLabel": "URL",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.authenticationLabel": "身份验证",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.commentsTextAreaFieldLabel": "其他注释",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.descriptionTextAreaFieldLabel": "描述",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.emailTextFieldLabel": "电子邮件",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.impactSelectFieldLabel": "影响",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.invalidApiUrlTextField": "URL 无效。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.mappingFieldComments": "注释",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.mappingFieldDescription": "描述",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.mappingFieldShortDescription": "简短描述",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.passwordTextFieldLabel": "密码",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.reenterValuesLabel": "用户名和密码已加密。请为这些字段重新输入值。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.rememberValuesLabel": "请记住这些值。每次编辑连接器时都必须重新输入。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredApiTokenTextField": "“Api 令牌”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredApiUrlTextField": "“URL”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredEmailTextField": "“电子邮件”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredPasswordTextField": "“密码”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredUsernameTextField": "“用户名”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requireHttpsApiUrlTextField": "URL 必须以 https:// 开头。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.selectMessageText": "在 ServiceNow 中创建事件。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.severitySelectFieldLabel": "严重性",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectHighOptionLabel": "高",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectLawOptionLabel": "低",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectMediumOptionLabel": "中",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.title": "事件",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.titleFieldLabel": "简短描述(必填)",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.urgencySelectFieldLabel": "紧急性",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.usernameTextFieldLabel": "用户名",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.serviceNowAction.apiUrlHelpLabel": "配置个人开发者实例",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.actionTypeTitle": "发送到 Slack",
|
||||
|
|
|
@ -12,7 +12,7 @@ import { getPagerDutyActionType } from './pagerduty';
|
|||
import { getWebhookActionType } from './webhook';
|
||||
import { TypeRegistry } from '../../type_registry';
|
||||
import { ActionTypeModel } from '../../../types';
|
||||
import { getServiceNowActionType } from './servicenow';
|
||||
import { getServiceNowITSMActionType, getServiceNowSIRActionType } from './servicenow';
|
||||
import { getJiraActionType } from './jira';
|
||||
import { getResilientActionType } from './resilient';
|
||||
import { getTeamsActionType } from './teams';
|
||||
|
@ -28,7 +28,8 @@ export function registerBuiltInActionTypes({
|
|||
actionTypeRegistry.register(getIndexActionType());
|
||||
actionTypeRegistry.register(getPagerDutyActionType());
|
||||
actionTypeRegistry.register(getWebhookActionType());
|
||||
actionTypeRegistry.register(getServiceNowActionType());
|
||||
actionTypeRegistry.register(getServiceNowITSMActionType());
|
||||
actionTypeRegistry.register(getServiceNowSIRActionType());
|
||||
actionTypeRegistry.register(getJiraActionType());
|
||||
actionTypeRegistry.register(getResilientActionType());
|
||||
actionTypeRegistry.register(getTeamsActionType());
|
||||
|
|
|
@ -8,6 +8,7 @@ import { httpServiceMock } from '../../../../../../../../src/core/public/mocks';
|
|||
import { getIssueTypes, getFieldsByIssueType, getIssues, getIssue } from './api';
|
||||
|
||||
const issueTypesResponse = {
|
||||
status: 'ok',
|
||||
data: {
|
||||
projects: [
|
||||
{
|
||||
|
@ -24,9 +25,11 @@ const issueTypesResponse = {
|
|||
},
|
||||
],
|
||||
},
|
||||
actionId: 'test',
|
||||
};
|
||||
|
||||
const fieldsResponse = {
|
||||
status: 'ok',
|
||||
data: {
|
||||
projects: [
|
||||
{
|
||||
|
@ -70,13 +73,18 @@ const fieldsResponse = {
|
|||
],
|
||||
},
|
||||
],
|
||||
actionId: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
const issueResponse = {
|
||||
id: '10267',
|
||||
key: 'RJ-107',
|
||||
fields: { summary: 'Test title' },
|
||||
status: 'ok',
|
||||
data: {
|
||||
id: '10267',
|
||||
key: 'RJ-107',
|
||||
fields: { summary: 'Test title' },
|
||||
},
|
||||
actionId: 'test',
|
||||
};
|
||||
|
||||
const issuesResponse = [issueResponse];
|
||||
|
|
|
@ -15,24 +15,4 @@ export const connectorConfiguration = {
|
|||
enabledInConfig: true,
|
||||
enabledInLicense: true,
|
||||
minimumLicenseRequired: 'gold',
|
||||
fields: {
|
||||
summary: {
|
||||
label: i18n.MAPPING_FIELD_SUMMARY,
|
||||
validSourceFields: ['title', 'description'],
|
||||
defaultSourceField: 'title',
|
||||
defaultActionType: 'overwrite',
|
||||
},
|
||||
description: {
|
||||
label: i18n.MAPPING_FIELD_DESC,
|
||||
validSourceFields: ['title', 'description'],
|
||||
defaultSourceField: 'description',
|
||||
defaultActionType: 'overwrite',
|
||||
},
|
||||
comments: {
|
||||
label: i18n.MAPPING_FIELD_COMMENTS,
|
||||
validSourceFields: ['comments'],
|
||||
defaultSourceField: 'comments',
|
||||
defaultActionType: 'append',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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 { httpServiceMock } from '../../../../../../../../src/core/public/mocks';
|
||||
import { getIncidentTypes, getSeverity } from './api';
|
||||
|
||||
const incidentTypesResponse = {
|
||||
status: 'ok',
|
||||
data: [
|
||||
{ id: 17, name: 'Communication error (fax; email)' },
|
||||
{ id: 1001, name: 'Custom type' },
|
||||
{ id: 21, name: 'Denial of Service' },
|
||||
{ id: 6, name: 'Improper disposal: digital asset(s)' },
|
||||
{ id: 7, name: 'Improper disposal: documents / files' },
|
||||
{ id: 4, name: 'Lost documents / files / records' },
|
||||
{ id: 3, name: 'Lost PC / laptop / tablet' },
|
||||
{ id: 1, name: 'Lost PDA / smartphone' },
|
||||
{ id: 8, name: 'Lost storage device / media' },
|
||||
{ id: 19, name: 'Malware' },
|
||||
{ id: 23, name: 'Not an Issue' },
|
||||
{ id: 18, name: 'Other' },
|
||||
{ id: 22, name: 'Phishing' },
|
||||
{ id: 11, name: 'Stolen documents / files / records' },
|
||||
{ id: 12, name: 'Stolen PC / laptop / tablet' },
|
||||
{ id: 13, name: 'Stolen PDA / smartphone' },
|
||||
{ id: 14, name: 'Stolen storage device / media' },
|
||||
{ id: 20, name: 'System Intrusion' },
|
||||
{ id: 16, name: 'TBD / Unknown' },
|
||||
{ id: 15, name: 'Vendor / 3rd party error' },
|
||||
],
|
||||
actionId: 'test',
|
||||
};
|
||||
|
||||
const severityResponse = {
|
||||
status: 'ok',
|
||||
data: [
|
||||
{ id: 4, name: 'Low' },
|
||||
{ id: 5, name: 'Medium' },
|
||||
{ id: 6, name: 'High' },
|
||||
],
|
||||
actionId: 'test',
|
||||
};
|
||||
|
||||
describe('Resilient API', () => {
|
||||
const http = httpServiceMock.createStartContract();
|
||||
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('getIncidentTypes', () => {
|
||||
test('should call get choices API', async () => {
|
||||
const abortCtrl = new AbortController();
|
||||
http.post.mockResolvedValueOnce(incidentTypesResponse);
|
||||
const res = await getIncidentTypes({
|
||||
http,
|
||||
signal: abortCtrl.signal,
|
||||
connectorId: 'test',
|
||||
});
|
||||
|
||||
expect(res).toEqual(incidentTypesResponse);
|
||||
expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', {
|
||||
body: '{"params":{"subAction":"incidentTypes","subActionParams":{}}}',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSeverity', () => {
|
||||
test('should call get choices API', async () => {
|
||||
const abortCtrl = new AbortController();
|
||||
http.post.mockResolvedValueOnce(severityResponse);
|
||||
const res = await getSeverity({
|
||||
http,
|
||||
signal: abortCtrl.signal,
|
||||
connectorId: 'test',
|
||||
});
|
||||
|
||||
expect(res).toEqual(severityResponse);
|
||||
expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', {
|
||||
body: '{"params":{"subAction":"severity","subActionParams":{}}}',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -15,24 +15,4 @@ export const connectorConfiguration = {
|
|||
enabledInConfig: true,
|
||||
enabledInLicense: true,
|
||||
minimumLicenseRequired: 'platinum',
|
||||
fields: {
|
||||
name: {
|
||||
label: i18n.MAPPING_FIELD_NAME,
|
||||
validSourceFields: ['title', 'description'],
|
||||
defaultSourceField: 'title',
|
||||
defaultActionType: 'overwrite',
|
||||
},
|
||||
description: {
|
||||
label: i18n.MAPPING_FIELD_DESC,
|
||||
validSourceFields: ['title', 'description'],
|
||||
defaultSourceField: 'description',
|
||||
defaultActionType: 'overwrite',
|
||||
},
|
||||
comments: {
|
||||
label: i18n.MAPPING_FIELD_COMMENTS,
|
||||
validSourceFields: ['comments'],
|
||||
defaultSourceField: 'comments',
|
||||
defaultActionType: 'append',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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 { httpServiceMock } from '../../../../../../../../src/core/public/mocks';
|
||||
import { getChoices } from './api';
|
||||
|
||||
const choicesResponse = {
|
||||
status: 'ok',
|
||||
data: [
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '1 - Critical',
|
||||
value: '1',
|
||||
element: 'priority',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '2 - High',
|
||||
value: '2',
|
||||
element: 'priority',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '3 - Moderate',
|
||||
value: '3',
|
||||
element: 'priority',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '4 - Low',
|
||||
value: '4',
|
||||
element: 'priority',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '5 - Planning',
|
||||
value: '5',
|
||||
element: 'priority',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('ServiceNow API', () => {
|
||||
const http = httpServiceMock.createStartContract();
|
||||
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('getChoices', () => {
|
||||
test('should call get choices API', async () => {
|
||||
const abortCtrl = new AbortController();
|
||||
http.post.mockResolvedValueOnce(choicesResponse);
|
||||
const res = await getChoices({
|
||||
http,
|
||||
signal: abortCtrl.signal,
|
||||
connectorId: 'test',
|
||||
fields: ['priority'],
|
||||
});
|
||||
|
||||
expect(res).toEqual(choicesResponse);
|
||||
expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', {
|
||||
body: '{"params":{"subAction":"getChoices","subActionParams":{"fields":["priority"]}}}',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 { HttpSetup } from 'kibana/public';
|
||||
import { BASE_ACTION_API_PATH } from '../../../constants';
|
||||
|
||||
export async function getChoices({
|
||||
http,
|
||||
signal,
|
||||
connectorId,
|
||||
fields,
|
||||
}: {
|
||||
http: HttpSetup;
|
||||
signal: AbortSignal;
|
||||
connectorId: string;
|
||||
fields: string[];
|
||||
}): Promise<Record<string, any>> {
|
||||
return await http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, {
|
||||
body: JSON.stringify({
|
||||
params: { subAction: 'getChoices', subActionParams: { fields } },
|
||||
}),
|
||||
signal,
|
||||
});
|
||||
}
|
|
@ -7,32 +7,24 @@
|
|||
import * as i18n from './translations';
|
||||
import logo from './logo.svg';
|
||||
|
||||
export const connectorConfiguration = {
|
||||
export const serviceNowITSMConfiguration = {
|
||||
id: '.servicenow',
|
||||
name: i18n.SERVICENOW_TITLE,
|
||||
name: i18n.SERVICENOW_ITSM_TITLE,
|
||||
desc: i18n.SERVICENOW_ITSM_DESC,
|
||||
logo,
|
||||
enabled: true,
|
||||
enabledInConfig: true,
|
||||
enabledInLicense: true,
|
||||
minimumLicenseRequired: 'platinum',
|
||||
};
|
||||
|
||||
export const serviceNowSIRConfiguration = {
|
||||
id: '.servicenow-sir',
|
||||
name: i18n.SERVICENOW_SIR_TITLE,
|
||||
desc: i18n.SERVICENOW_SIR_DESC,
|
||||
logo,
|
||||
enabled: true,
|
||||
enabledInConfig: true,
|
||||
enabledInLicense: true,
|
||||
minimumLicenseRequired: 'platinum',
|
||||
fields: {
|
||||
short_description: {
|
||||
label: i18n.MAPPING_FIELD_SHORT_DESC,
|
||||
validSourceFields: ['title', 'description'],
|
||||
defaultSourceField: 'title',
|
||||
defaultActionType: 'overwrite',
|
||||
},
|
||||
description: {
|
||||
label: i18n.MAPPING_FIELD_DESC,
|
||||
validSourceFields: ['title', 'description'],
|
||||
defaultSourceField: 'description',
|
||||
defaultActionType: 'overwrite',
|
||||
},
|
||||
comments: {
|
||||
label: i18n.MAPPING_FIELD_COMMENTS,
|
||||
validSourceFields: ['comments'],
|
||||
defaultSourceField: 'comments',
|
||||
defaultActionType: 'append',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { getActionType as getServiceNowActionType } from './servicenow';
|
||||
export { getServiceNowITSMActionType, getServiceNowSIRActionType } from './servicenow';
|
||||
|
|
|
@ -8,102 +8,110 @@ import { registerBuiltInActionTypes } from '.././index';
|
|||
import { ActionTypeModel } from '../../../../types';
|
||||
import { ServiceNowActionConnector } from './types';
|
||||
|
||||
const ACTION_TYPE_ID = '.servicenow';
|
||||
let actionTypeModel: ActionTypeModel;
|
||||
const SERVICENOW_ITSM_ACTION_TYPE_ID = '.servicenow';
|
||||
const SERVICENOW_SIR_ACTION_TYPE_ID = '.servicenow-sir';
|
||||
let actionTypeRegistry: TypeRegistry<ActionTypeModel>;
|
||||
|
||||
beforeAll(() => {
|
||||
const actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
|
||||
actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
|
||||
registerBuiltInActionTypes({ actionTypeRegistry });
|
||||
const getResult = actionTypeRegistry.get(ACTION_TYPE_ID);
|
||||
if (getResult !== null) {
|
||||
actionTypeModel = getResult;
|
||||
}
|
||||
});
|
||||
|
||||
describe('actionTypeRegistry.get() works', () => {
|
||||
test('action type static data is as expected', () => {
|
||||
expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID);
|
||||
[SERVICENOW_ITSM_ACTION_TYPE_ID, SERVICENOW_SIR_ACTION_TYPE_ID].forEach((id) => {
|
||||
test(`${id}: action type static data is as expected`, () => {
|
||||
const actionTypeModel = actionTypeRegistry.get(id);
|
||||
expect(actionTypeModel.id).toEqual(id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('servicenow connector validation', () => {
|
||||
test('connector validation succeeds when connector config is valid', () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.servicenow',
|
||||
name: 'ServiceNow',
|
||||
isPreconfigured: false,
|
||||
config: {
|
||||
apiUrl: 'https://dev94428.service-now.com/',
|
||||
},
|
||||
} as ServiceNowActionConnector;
|
||||
[SERVICENOW_ITSM_ACTION_TYPE_ID, SERVICENOW_SIR_ACTION_TYPE_ID].forEach((id) => {
|
||||
test(`${id}: connector validation succeeds when connector config is valid`, () => {
|
||||
const actionTypeModel = actionTypeRegistry.get(id);
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: id,
|
||||
name: 'ServiceNow',
|
||||
isPreconfigured: false,
|
||||
config: {
|
||||
apiUrl: 'https://dev94428.service-now.com/',
|
||||
},
|
||||
} as ServiceNowActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
apiUrl: [],
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
apiUrl: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
username: [],
|
||||
password: [],
|
||||
secrets: {
|
||||
errors: {
|
||||
username: [],
|
||||
password: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when connector config is not valid', () => {
|
||||
const actionConnector = ({
|
||||
secrets: {
|
||||
username: 'user',
|
||||
},
|
||||
id: '.servicenow',
|
||||
actionTypeId: '.servicenow',
|
||||
name: 'servicenow',
|
||||
config: {},
|
||||
} as unknown) as ServiceNowActionConnector;
|
||||
test(`${id}: connector validation fails when connector config is not valid`, () => {
|
||||
const actionTypeModel = actionTypeRegistry.get(id);
|
||||
const actionConnector = ({
|
||||
secrets: {
|
||||
username: 'user',
|
||||
},
|
||||
id,
|
||||
actionTypeId: id,
|
||||
name: 'servicenow',
|
||||
config: {},
|
||||
} as unknown) as ServiceNowActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
apiUrl: ['URL is required.'],
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
apiUrl: ['URL is required.'],
|
||||
},
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
username: [],
|
||||
password: ['Password is required.'],
|
||||
secrets: {
|
||||
errors: {
|
||||
username: [],
|
||||
password: ['Password is required.'],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('servicenow action params validation', () => {
|
||||
test('action params validation succeeds when action params is valid', () => {
|
||||
const actionParams = {
|
||||
subActionParams: { incident: { short_description: 'some title {{test}}' }, comments: [] },
|
||||
};
|
||||
[SERVICENOW_ITSM_ACTION_TYPE_ID, SERVICENOW_SIR_ACTION_TYPE_ID].forEach((id) => {
|
||||
test(`${id}: action params validation succeeds when action params is valid`, () => {
|
||||
const actionTypeModel = actionTypeRegistry.get(id);
|
||||
const actionParams = {
|
||||
subActionParams: { incident: { short_description: 'some title {{test}}' }, comments: [] },
|
||||
};
|
||||
|
||||
expect(actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: { ['subActionParams.incident.short_description']: [] },
|
||||
expect(actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: { ['subActionParams.incident.short_description']: [] },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('params validation fails when body is not valid', () => {
|
||||
const actionParams = {
|
||||
subActionParams: { incident: { short_description: '' }, comments: [] },
|
||||
};
|
||||
test(`${id}: params validation fails when body is not valid`, () => {
|
||||
const actionTypeModel = actionTypeRegistry.get(id);
|
||||
const actionParams = {
|
||||
subActionParams: { incident: { short_description: '' }, comments: [] },
|
||||
};
|
||||
|
||||
expect(actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
['subActionParams.incident.short_description']: ['Short description is required.'],
|
||||
},
|
||||
expect(actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
['subActionParams.incident.short_description']: ['Short description is required.'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,13 +10,14 @@ import {
|
|||
ActionTypeModel,
|
||||
ConnectorValidationResult,
|
||||
} from '../../../../types';
|
||||
import { connectorConfiguration } from './config';
|
||||
import { serviceNowITSMConfiguration, serviceNowSIRConfiguration } from './config';
|
||||
import logo from './logo.svg';
|
||||
import {
|
||||
ServiceNowActionConnector,
|
||||
ServiceNowConfig,
|
||||
ServiceNowSecrets,
|
||||
ServiceNowActionParams,
|
||||
ServiceNowITSMActionParams,
|
||||
ServiceNowSIRActionParams,
|
||||
} from './types';
|
||||
import * as i18n from './translations';
|
||||
import { isValidUrl } from '../../../lib/value_validators';
|
||||
|
@ -60,19 +61,21 @@ const validateConnector = (
|
|||
return validationResult;
|
||||
};
|
||||
|
||||
export function getActionType(): ActionTypeModel<
|
||||
export function getServiceNowITSMActionType(): ActionTypeModel<
|
||||
ServiceNowConfig,
|
||||
ServiceNowSecrets,
|
||||
ServiceNowActionParams
|
||||
ServiceNowITSMActionParams
|
||||
> {
|
||||
return {
|
||||
id: connectorConfiguration.id,
|
||||
id: serviceNowITSMConfiguration.id,
|
||||
iconClass: logo,
|
||||
selectMessage: i18n.SERVICENOW_DESC,
|
||||
actionTypeTitle: connectorConfiguration.name,
|
||||
selectMessage: serviceNowITSMConfiguration.desc,
|
||||
actionTypeTitle: serviceNowITSMConfiguration.name,
|
||||
validateConnector,
|
||||
actionConnectorFields: lazy(() => import('./servicenow_connectors')),
|
||||
validateParams: (actionParams: ServiceNowActionParams): GenericValidationResult<unknown> => {
|
||||
validateParams: (
|
||||
actionParams: ServiceNowITSMActionParams
|
||||
): GenericValidationResult<unknown> => {
|
||||
const errors = {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'subActionParams.incident.short_description': new Array<string>(),
|
||||
|
@ -89,6 +92,39 @@ export function getActionType(): ActionTypeModel<
|
|||
}
|
||||
return validationResult;
|
||||
},
|
||||
actionParamsFields: lazy(() => import('./servicenow_params')),
|
||||
actionParamsFields: lazy(() => import('./servicenow_itsm_params')),
|
||||
};
|
||||
}
|
||||
|
||||
export function getServiceNowSIRActionType(): ActionTypeModel<
|
||||
ServiceNowConfig,
|
||||
ServiceNowSecrets,
|
||||
ServiceNowSIRActionParams
|
||||
> {
|
||||
return {
|
||||
id: serviceNowSIRConfiguration.id,
|
||||
iconClass: logo,
|
||||
selectMessage: serviceNowSIRConfiguration.desc,
|
||||
actionTypeTitle: serviceNowSIRConfiguration.name,
|
||||
validateConnector,
|
||||
actionConnectorFields: lazy(() => import('./servicenow_connectors')),
|
||||
validateParams: (actionParams: ServiceNowSIRActionParams): GenericValidationResult<unknown> => {
|
||||
const errors = {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'subActionParams.incident.short_description': new Array<string>(),
|
||||
};
|
||||
const validationResult = {
|
||||
errors,
|
||||
};
|
||||
if (
|
||||
actionParams.subActionParams &&
|
||||
actionParams.subActionParams.incident &&
|
||||
!actionParams.subActionParams.incident.short_description?.length
|
||||
) {
|
||||
errors['subActionParams.incident.short_description'].push(i18n.TITLE_REQUIRED);
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
actionParamsFields: lazy(() => import('./servicenow_sir_params')),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,8 +5,18 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import ServiceNowParamsFields from './servicenow_params';
|
||||
import { act } from '@testing-library/react';
|
||||
|
||||
import { ActionConnector } from '../../../../types';
|
||||
import { useGetChoices } from './use_get_choices';
|
||||
import ServiceNowITSMParamsFields from './servicenow_itsm_params';
|
||||
import { Choice } from './types';
|
||||
|
||||
jest.mock('./use_get_choices');
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
const useGetChoicesMock = useGetChoices as jest.Mock;
|
||||
|
||||
const actionParams = {
|
||||
subAction: 'pushToService',
|
||||
subActionParams: {
|
||||
|
@ -16,7 +26,6 @@ const actionParams = {
|
|||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
savedObjectId: '123',
|
||||
externalId: null,
|
||||
},
|
||||
comments: [],
|
||||
|
@ -31,6 +40,7 @@ const connector: ActionConnector = {
|
|||
name: 'Test',
|
||||
isPreconfigured: false,
|
||||
};
|
||||
|
||||
const editAction = jest.fn();
|
||||
const defaultProps = {
|
||||
actionConnector: connector,
|
||||
|
@ -40,31 +50,71 @@ const defaultProps = {
|
|||
index: 0,
|
||||
messageVariables: [],
|
||||
};
|
||||
describe('ServiceNowParamsFields renders', () => {
|
||||
|
||||
const useGetChoicesResponse = {
|
||||
isLoading: false,
|
||||
choices: ['severity', 'urgency', 'impact']
|
||||
.map((element) => [
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '1 - Critical',
|
||||
value: '1',
|
||||
element,
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '2 - High',
|
||||
value: '2',
|
||||
element,
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '3 - Moderate',
|
||||
value: '3',
|
||||
element,
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '4 - Low',
|
||||
value: '4',
|
||||
element,
|
||||
},
|
||||
])
|
||||
.flat(),
|
||||
};
|
||||
|
||||
describe('ServiceNowITSMParamsFields renders', () => {
|
||||
let onChoices = (choices: Choice[]) => {};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
useGetChoicesMock.mockImplementation((args) => {
|
||||
onChoices = args.onSuccess;
|
||||
return useGetChoicesResponse;
|
||||
});
|
||||
});
|
||||
|
||||
test('all params fields is rendered', () => {
|
||||
const wrapper = mount(<ServiceNowParamsFields {...defaultProps} />);
|
||||
expect(wrapper.find('[data-test-subj="urgencySelect"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="severitySelect"]').first().prop('value')).toStrictEqual(
|
||||
'1'
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="impactSelect"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="short_descriptionInput"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="descriptionTextArea"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="commentsTextArea"]').length > 0).toBeTruthy();
|
||||
const wrapper = mount(<ServiceNowITSMParamsFields {...defaultProps} />);
|
||||
expect(wrapper.find('[data-test-subj="urgencySelect"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="severitySelect"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="impactSelect"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="short_descriptionInput"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="descriptionTextArea"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="commentsTextArea"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('If short_description has errors, form row is invalid', () => {
|
||||
const newProps = {
|
||||
...defaultProps,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
errors: { 'subActionParams.incident.short_description': ['error'] },
|
||||
};
|
||||
const wrapper = mount(<ServiceNowParamsFields {...newProps} />);
|
||||
const wrapper = mount(<ServiceNowITSMParamsFields {...newProps} />);
|
||||
const title = wrapper.find('[data-test-subj="short_descriptionInput"]').first();
|
||||
expect(title.prop('isInvalid')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('When subActionParams is undefined, set to default', () => {
|
||||
const { subActionParams, ...newParams } = actionParams;
|
||||
|
||||
|
@ -72,12 +122,13 @@ describe('ServiceNowParamsFields renders', () => {
|
|||
...defaultProps,
|
||||
actionParams: newParams,
|
||||
};
|
||||
mount(<ServiceNowParamsFields {...newProps} />);
|
||||
mount(<ServiceNowITSMParamsFields {...newProps} />);
|
||||
expect(editAction.mock.calls[0][1]).toEqual({
|
||||
incident: {},
|
||||
comments: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('When subAction is undefined, set to default', () => {
|
||||
const { subAction, ...newParams } = actionParams;
|
||||
|
||||
|
@ -85,11 +136,12 @@ describe('ServiceNowParamsFields renders', () => {
|
|||
...defaultProps,
|
||||
actionParams: newParams,
|
||||
};
|
||||
mount(<ServiceNowParamsFields {...newProps} />);
|
||||
mount(<ServiceNowITSMParamsFields {...newProps} />);
|
||||
expect(editAction.mock.calls[0][1]).toEqual('pushToService');
|
||||
});
|
||||
|
||||
test('Resets fields when connector changes', () => {
|
||||
const wrapper = mount(<ServiceNowParamsFields {...defaultProps} />);
|
||||
const wrapper = mount(<ServiceNowITSMParamsFields {...defaultProps} />);
|
||||
expect(editAction.mock.calls.length).toEqual(0);
|
||||
wrapper.setProps({ actionConnector: { ...connector, id: '1234' } });
|
||||
expect(editAction.mock.calls.length).toEqual(1);
|
||||
|
@ -98,6 +150,52 @@ describe('ServiceNowParamsFields renders', () => {
|
|||
comments: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('it transforms the urgencies to options correctly', async () => {
|
||||
const wrapper = mount(<ServiceNowITSMParamsFields {...defaultProps} />);
|
||||
act(() => {
|
||||
onChoices(useGetChoicesResponse.choices);
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test-subj="urgencySelect"]').first().prop('options')).toEqual([
|
||||
{ value: '1', text: '1 - Critical' },
|
||||
{ value: '2', text: '2 - High' },
|
||||
{ value: '3', text: '3 - Moderate' },
|
||||
{ value: '4', text: '4 - Low' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('it transforms the severities to options correctly', async () => {
|
||||
const wrapper = mount(<ServiceNowITSMParamsFields {...defaultProps} />);
|
||||
act(() => {
|
||||
onChoices(useGetChoicesResponse.choices);
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test-subj="severitySelect"]').first().prop('options')).toEqual([
|
||||
{ value: '1', text: '1 - Critical' },
|
||||
{ value: '2', text: '2 - High' },
|
||||
{ value: '3', text: '3 - Moderate' },
|
||||
{ value: '4', text: '4 - Low' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('it transforms the impacts to options correctly', async () => {
|
||||
const wrapper = mount(<ServiceNowITSMParamsFields {...defaultProps} />);
|
||||
act(() => {
|
||||
onChoices(useGetChoicesResponse.choices);
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test-subj="impactSelect"]').first().prop('options')).toEqual([
|
||||
{ value: '1', text: '1 - Critical' },
|
||||
{ value: '2', text: '2 - High' },
|
||||
{ value: '3', text: '3 - Moderate' },
|
||||
{ value: '4', text: '4 - Low' },
|
||||
]);
|
||||
});
|
||||
|
||||
describe('UI updates', () => {
|
||||
const changeEvent = { target: { value: 'Bug' } } as React.ChangeEvent<HTMLSelectElement>;
|
||||
const simpleFields = [
|
||||
|
@ -107,22 +205,25 @@ describe('ServiceNowParamsFields renders', () => {
|
|||
{ dataTestSubj: '[data-test-subj="severitySelect"]', key: 'severity' },
|
||||
{ dataTestSubj: '[data-test-subj="impactSelect"]', key: 'impact' },
|
||||
];
|
||||
|
||||
simpleFields.forEach((field) =>
|
||||
test(`${field.key} update triggers editAction :D`, () => {
|
||||
const wrapper = mount(<ServiceNowParamsFields {...defaultProps} />);
|
||||
const wrapper = mount(<ServiceNowITSMParamsFields {...defaultProps} />);
|
||||
const theField = wrapper.find(field.dataTestSubj).first();
|
||||
theField.prop('onChange')!(changeEvent);
|
||||
expect(editAction.mock.calls[0][1].incident[field.key]).toEqual(changeEvent.target.value);
|
||||
})
|
||||
);
|
||||
|
||||
test('A comment triggers editAction', () => {
|
||||
const wrapper = mount(<ServiceNowParamsFields {...defaultProps} />);
|
||||
const wrapper = mount(<ServiceNowITSMParamsFields {...defaultProps} />);
|
||||
const comments = wrapper.find('textarea[data-test-subj="commentsTextArea"]');
|
||||
expect(comments.simulate('change', changeEvent));
|
||||
expect(editAction.mock.calls[0][1].comments.length).toEqual(1);
|
||||
});
|
||||
|
||||
test('An empty comment does not trigger editAction', () => {
|
||||
const wrapper = mount(<ServiceNowParamsFields {...defaultProps} />);
|
||||
const wrapper = mount(<ServiceNowITSMParamsFields {...defaultProps} />);
|
||||
const emptyComment = { target: { value: '' } };
|
||||
const comments = wrapper.find('[data-test-subj="commentsTextArea"] textarea');
|
||||
expect(comments.simulate('change', emptyComment));
|
|
@ -4,8 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Fragment, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
EuiFormRow,
|
||||
EuiSelect,
|
||||
|
@ -14,38 +13,29 @@ import {
|
|||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { ActionParamsProps } from '../../../../types';
|
||||
import { ServiceNowActionParams } from './types';
|
||||
import { ServiceNowITSMActionParams, Choice, Options } from './types';
|
||||
import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables';
|
||||
import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables';
|
||||
import { useGetChoices } from './use_get_choices';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const selectOptions = [
|
||||
{
|
||||
value: '1',
|
||||
text: i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectHighOptionLabel',
|
||||
{ defaultMessage: 'High' }
|
||||
),
|
||||
},
|
||||
{
|
||||
value: '2',
|
||||
text: i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectMediumOptionLabel',
|
||||
{ defaultMessage: 'Medium' }
|
||||
),
|
||||
},
|
||||
{
|
||||
value: '3',
|
||||
text: i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectLawOptionLabel',
|
||||
{ defaultMessage: 'Low' }
|
||||
),
|
||||
},
|
||||
];
|
||||
const useGetChoicesFields = ['urgency', 'severity', 'impact'];
|
||||
const defaultOptions: Options = {
|
||||
urgency: [],
|
||||
severity: [],
|
||||
impact: [],
|
||||
};
|
||||
|
||||
const ServiceNowParamsFields: React.FunctionComponent<
|
||||
ActionParamsProps<ServiceNowActionParams>
|
||||
ActionParamsProps<ServiceNowITSMActionParams>
|
||||
> = ({ actionConnector, actionParams, editAction, index, errors, messageVariables }) => {
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
const actionConnectorRef = useRef(actionConnector?.id ?? '');
|
||||
const { incident, comments } = useMemo(
|
||||
() =>
|
||||
|
@ -53,10 +43,12 @@ const ServiceNowParamsFields: React.FunctionComponent<
|
|||
(({
|
||||
incident: {},
|
||||
comments: [],
|
||||
} as unknown) as ServiceNowActionParams['subActionParams']),
|
||||
} as unknown) as ServiceNowITSMActionParams['subActionParams']),
|
||||
[actionParams.subActionParams]
|
||||
);
|
||||
|
||||
const [options, setOptions] = useState<Options>(defaultOptions);
|
||||
|
||||
const editSubActionProperty = useCallback(
|
||||
(key: string, value: any) => {
|
||||
const newProps =
|
||||
|
@ -80,6 +72,28 @@ const ServiceNowParamsFields: React.FunctionComponent<
|
|||
[editSubActionProperty]
|
||||
);
|
||||
|
||||
const onChoicesSuccess = (choices: Choice[]) =>
|
||||
setOptions(
|
||||
choices.reduce(
|
||||
(acc, choice) => ({
|
||||
...acc,
|
||||
[choice.element]: [
|
||||
...(acc[choice.element] != null ? acc[choice.element] : []),
|
||||
{ value: choice.value, text: choice.label },
|
||||
],
|
||||
}),
|
||||
defaultOptions
|
||||
)
|
||||
);
|
||||
|
||||
const { isLoading: isLoadingChoices } = useGetChoices({
|
||||
http,
|
||||
toastNotifications: toasts,
|
||||
actionConnector,
|
||||
fields: useGetChoicesFields,
|
||||
onSuccess: onChoicesSuccess,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (actionConnector != null && actionConnectorRef.current !== actionConnector.id) {
|
||||
actionConnectorRef.current = actionConnector.id;
|
||||
|
@ -94,6 +108,7 @@ const ServiceNowParamsFields: React.FunctionComponent<
|
|||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [actionConnector]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!actionParams.subAction) {
|
||||
editAction('subAction', 'pushToService', index);
|
||||
|
@ -114,64 +129,47 @@ const ServiceNowParamsFields: React.FunctionComponent<
|
|||
return (
|
||||
<Fragment>
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.title',
|
||||
{ defaultMessage: 'Incident' }
|
||||
)}
|
||||
</h3>
|
||||
<h3>{i18n.INCIDENT}</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.urgencySelectFieldLabel',
|
||||
{ defaultMessage: 'Urgency' }
|
||||
)}
|
||||
>
|
||||
<EuiFormRow fullWidth label={i18n.URGENCY_LABEL}>
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
data-test-subj="urgencySelect"
|
||||
hasNoInitialSelection
|
||||
options={selectOptions}
|
||||
value={incident.urgency ?? undefined}
|
||||
isLoading={isLoadingChoices}
|
||||
disabled={isLoadingChoices}
|
||||
options={options.urgency}
|
||||
value={incident.urgency ?? ''}
|
||||
onChange={(e) => editSubActionProperty('urgency', e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.severitySelectFieldLabel',
|
||||
{ defaultMessage: 'Severity' }
|
||||
)}
|
||||
>
|
||||
<EuiFormRow fullWidth label={i18n.SEVERITY_LABEL}>
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
data-test-subj="severitySelect"
|
||||
hasNoInitialSelection
|
||||
options={selectOptions}
|
||||
value={incident.severity ?? undefined}
|
||||
isLoading={isLoadingChoices}
|
||||
disabled={isLoadingChoices}
|
||||
options={options.severity}
|
||||
value={incident.severity ?? ''}
|
||||
onChange={(e) => editSubActionProperty('severity', e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.impactSelectFieldLabel',
|
||||
{ defaultMessage: 'Impact' }
|
||||
)}
|
||||
>
|
||||
<EuiFormRow fullWidth label={i18n.IMPACT_LABEL}>
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
data-test-subj="impactSelect"
|
||||
hasNoInitialSelection
|
||||
options={selectOptions}
|
||||
value={incident.impact ?? undefined}
|
||||
isLoading={isLoadingChoices}
|
||||
disabled={isLoadingChoices}
|
||||
options={options.impact}
|
||||
value={incident.impact ?? ''}
|
||||
onChange={(e) => editSubActionProperty('impact', e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
@ -185,10 +183,7 @@ const ServiceNowParamsFields: React.FunctionComponent<
|
|||
errors['subActionParams.incident.short_description'].length > 0 &&
|
||||
incident.short_description !== undefined
|
||||
}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.titleFieldLabel',
|
||||
{ defaultMessage: 'Short description (required)' }
|
||||
)}
|
||||
label={i18n.SHORT_DESCRIPTION_LABEL}
|
||||
>
|
||||
<TextFieldWithMessageVariables
|
||||
index={index}
|
||||
|
@ -205,10 +200,7 @@ const ServiceNowParamsFields: React.FunctionComponent<
|
|||
messageVariables={messageVariables}
|
||||
paramsProperty={'description'}
|
||||
inputTargetValue={incident.description ?? undefined}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.descriptionTextAreaFieldLabel',
|
||||
{ defaultMessage: 'Description' }
|
||||
)}
|
||||
label={i18n.DESCRIPTION_LABEL}
|
||||
/>
|
||||
<TextAreaWithMessageVariables
|
||||
index={index}
|
||||
|
@ -216,10 +208,7 @@ const ServiceNowParamsFields: React.FunctionComponent<
|
|||
messageVariables={messageVariables}
|
||||
paramsProperty={'comments'}
|
||||
inputTargetValue={comments && comments.length > 0 ? comments[0].comment : undefined}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.commentsTextAreaFieldLabel',
|
||||
{ defaultMessage: 'Additional comments' }
|
||||
)}
|
||||
label={i18n.COMMENTS_LABEL}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
|
@ -0,0 +1,311 @@
|
|||
/*
|
||||
* 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 { mount } from 'enzyme';
|
||||
import { act } from '@testing-library/react';
|
||||
|
||||
import { ActionConnector } from '../../../../types';
|
||||
import { useGetChoices } from './use_get_choices';
|
||||
import ServiceNowSIRParamsFields from './servicenow_sir_params';
|
||||
import { Choice } from './types';
|
||||
|
||||
jest.mock('./use_get_choices');
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
const useGetChoicesMock = useGetChoices as jest.Mock;
|
||||
|
||||
const actionParams = {
|
||||
subAction: 'pushToService',
|
||||
subActionParams: {
|
||||
incident: {
|
||||
short_description: 'sn title',
|
||||
description: 'some description',
|
||||
category: 'Denial of Service',
|
||||
dest_ip: '192.168.1.1',
|
||||
source_ip: '192.168.1.2',
|
||||
malware_hash: '098f6bcd4621d373cade4e832627b4f6',
|
||||
malware_url: 'https://attack.com',
|
||||
priority: '1',
|
||||
subcategory: '20',
|
||||
externalId: null,
|
||||
},
|
||||
comments: [],
|
||||
},
|
||||
};
|
||||
|
||||
const connector: ActionConnector = {
|
||||
secrets: {},
|
||||
config: {},
|
||||
id: 'test',
|
||||
actionTypeId: '.test',
|
||||
name: 'Test',
|
||||
isPreconfigured: false,
|
||||
};
|
||||
|
||||
const editAction = jest.fn();
|
||||
const defaultProps = {
|
||||
actionConnector: connector,
|
||||
actionParams,
|
||||
errors: { ['subActionParams.incident.short_description']: [] },
|
||||
editAction,
|
||||
index: 0,
|
||||
messageVariables: [],
|
||||
};
|
||||
|
||||
const choicesResponse = {
|
||||
isLoading: false,
|
||||
choices: [
|
||||
{
|
||||
dependent_value: '',
|
||||
label: 'Priviledge Escalation',
|
||||
value: 'Priviledge Escalation',
|
||||
element: 'category',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: 'Criminal activity/investigation',
|
||||
value: 'Criminal activity/investigation',
|
||||
element: 'category',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: 'Denial of Service',
|
||||
value: 'Denial of Service',
|
||||
element: 'category',
|
||||
},
|
||||
{
|
||||
dependent_value: 'Denial of Service',
|
||||
label: 'Inbound or outbound',
|
||||
value: '12',
|
||||
element: 'subcategory',
|
||||
},
|
||||
{
|
||||
dependent_value: 'Denial of Service',
|
||||
label: 'Single or distributed (DoS or DDoS)',
|
||||
value: '26',
|
||||
element: 'subcategory',
|
||||
},
|
||||
{
|
||||
dependent_value: 'Denial of Service',
|
||||
label: 'Inbound DDos',
|
||||
value: 'inbound_ddos',
|
||||
element: 'subcategory',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '1 - Critical',
|
||||
value: '1',
|
||||
element: 'priority',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '2 - High',
|
||||
value: '2',
|
||||
element: 'priority',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '3 - Moderate',
|
||||
value: '3',
|
||||
element: 'priority',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '4 - Low',
|
||||
value: '4',
|
||||
element: 'priority',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '5 - Planning',
|
||||
value: '5',
|
||||
element: 'priority',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('ServiceNowSIRParamsFields renders', () => {
|
||||
let onChoicesSuccess = (choices: Choice[]) => {};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
useGetChoicesMock.mockImplementation((args) => {
|
||||
onChoicesSuccess = args.onSuccess;
|
||||
return choicesResponse;
|
||||
});
|
||||
});
|
||||
|
||||
test('all params fields is rendered', () => {
|
||||
const wrapper = mount(<ServiceNowSIRParamsFields {...defaultProps} />);
|
||||
expect(wrapper.find('[data-test-subj="short_descriptionInput"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="source_ipInput"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="dest_ipInput"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="malware_urlInput"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="malware_hashInput"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="prioritySelect"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="categorySelect"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="subcategorySelect"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="descriptionTextArea"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="commentsTextArea"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('If short_description has errors, form row is invalid', () => {
|
||||
const newProps = {
|
||||
...defaultProps,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
errors: { 'subActionParams.incident.short_description': ['error'] },
|
||||
};
|
||||
const wrapper = mount(<ServiceNowSIRParamsFields {...newProps} />);
|
||||
const title = wrapper.find('[data-test-subj="short_descriptionInput"]').first();
|
||||
expect(title.prop('isInvalid')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('When subActionParams is undefined, set to default', () => {
|
||||
const { subActionParams, ...newParams } = actionParams;
|
||||
|
||||
const newProps = {
|
||||
...defaultProps,
|
||||
actionParams: newParams,
|
||||
};
|
||||
mount(<ServiceNowSIRParamsFields {...newProps} />);
|
||||
expect(editAction.mock.calls[0][1]).toEqual({
|
||||
incident: {},
|
||||
comments: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('When subAction is undefined, set to default', () => {
|
||||
const { subAction, ...newParams } = actionParams;
|
||||
|
||||
const newProps = {
|
||||
...defaultProps,
|
||||
actionParams: newParams,
|
||||
};
|
||||
mount(<ServiceNowSIRParamsFields {...newProps} />);
|
||||
expect(editAction.mock.calls[0][1]).toEqual('pushToService');
|
||||
});
|
||||
|
||||
test('Resets fields when connector changes', () => {
|
||||
const wrapper = mount(<ServiceNowSIRParamsFields {...defaultProps} />);
|
||||
expect(editAction.mock.calls.length).toEqual(0);
|
||||
wrapper.setProps({ actionConnector: { ...connector, id: '1234' } });
|
||||
expect(editAction.mock.calls.length).toEqual(1);
|
||||
expect(editAction.mock.calls[0][1]).toEqual({
|
||||
incident: {},
|
||||
comments: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('it transforms the categories to options correctly', async () => {
|
||||
const wrapper = mount(<ServiceNowSIRParamsFields {...defaultProps} />);
|
||||
act(() => {
|
||||
onChoicesSuccess(choicesResponse.choices);
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test-subj="categorySelect"]').first().prop('options')).toEqual([
|
||||
{ value: 'Priviledge Escalation', text: 'Priviledge Escalation' },
|
||||
{
|
||||
value: 'Criminal activity/investigation',
|
||||
text: 'Criminal activity/investigation',
|
||||
},
|
||||
{ value: 'Denial of Service', text: 'Denial of Service' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('it transforms the subcategories to options correctly', async () => {
|
||||
const wrapper = mount(<ServiceNowSIRParamsFields {...defaultProps} />);
|
||||
act(() => {
|
||||
onChoicesSuccess(choicesResponse.choices);
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test-subj="subcategorySelect"]').first().prop('options')).toEqual([
|
||||
{
|
||||
text: 'Inbound or outbound',
|
||||
value: '12',
|
||||
},
|
||||
{
|
||||
text: 'Single or distributed (DoS or DDoS)',
|
||||
value: '26',
|
||||
},
|
||||
{
|
||||
text: 'Inbound DDos',
|
||||
value: 'inbound_ddos',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('it transforms the priorities to options correctly', async () => {
|
||||
const wrapper = mount(<ServiceNowSIRParamsFields {...defaultProps} />);
|
||||
act(() => {
|
||||
onChoicesSuccess(choicesResponse.choices);
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test-subj="prioritySelect"]').first().prop('options')).toEqual([
|
||||
{
|
||||
text: '1 - Critical',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
text: '2 - High',
|
||||
value: '2',
|
||||
},
|
||||
{
|
||||
text: '3 - Moderate',
|
||||
value: '3',
|
||||
},
|
||||
{
|
||||
text: '4 - Low',
|
||||
value: '4',
|
||||
},
|
||||
{
|
||||
text: '5 - Planning',
|
||||
value: '5',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('UI updates', () => {
|
||||
const changeEvent = { target: { value: 'Bug' } } as React.ChangeEvent<HTMLSelectElement>;
|
||||
const simpleFields = [
|
||||
{ dataTestSubj: 'input[data-test-subj="short_descriptionInput"]', key: 'short_description' },
|
||||
{ dataTestSubj: 'textarea[data-test-subj="descriptionTextArea"]', key: 'description' },
|
||||
{ dataTestSubj: '[data-test-subj="source_ipInput"]', key: 'source_ip' },
|
||||
{ dataTestSubj: '[data-test-subj="dest_ipInput"]', key: 'dest_ip' },
|
||||
{ dataTestSubj: '[data-test-subj="malware_urlInput"]', key: 'malware_url' },
|
||||
{ dataTestSubj: '[data-test-subj="malware_hashInput"]', key: 'malware_hash' },
|
||||
{ dataTestSubj: '[data-test-subj="prioritySelect"]', key: 'priority' },
|
||||
{ dataTestSubj: '[data-test-subj="categorySelect"]', key: 'category' },
|
||||
{ dataTestSubj: '[data-test-subj="subcategorySelect"]', key: 'subcategory' },
|
||||
];
|
||||
|
||||
simpleFields.forEach((field) =>
|
||||
test(`${field.key} update triggers editAction :D`, () => {
|
||||
const wrapper = mount(<ServiceNowSIRParamsFields {...defaultProps} />);
|
||||
const theField = wrapper.find(field.dataTestSubj).first();
|
||||
theField.prop('onChange')!(changeEvent);
|
||||
expect(editAction.mock.calls[0][1].incident[field.key]).toEqual(changeEvent.target.value);
|
||||
})
|
||||
);
|
||||
|
||||
test('A comment triggers editAction', () => {
|
||||
const wrapper = mount(<ServiceNowSIRParamsFields {...defaultProps} />);
|
||||
const comments = wrapper.find('textarea[data-test-subj="commentsTextArea"]');
|
||||
expect(comments.simulate('change', changeEvent));
|
||||
expect(editAction.mock.calls[0][1].comments.length).toEqual(1);
|
||||
});
|
||||
|
||||
test('An empty comment does not trigger editAction', () => {
|
||||
const wrapper = mount(<ServiceNowSIRParamsFields {...defaultProps} />);
|
||||
const emptyComment = { target: { value: '' } };
|
||||
const comments = wrapper.find('[data-test-subj="commentsTextArea"] textarea');
|
||||
expect(comments.simulate('change', emptyComment));
|
||||
expect(editAction.mock.calls.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,295 @@
|
|||
/*
|
||||
* 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, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
EuiFormRow,
|
||||
EuiSelect,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
EuiSelectOption,
|
||||
} from '@elastic/eui';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { ActionParamsProps } from '../../../../types';
|
||||
import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables';
|
||||
import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { useGetChoices } from './use_get_choices';
|
||||
import { ServiceNowSIRActionParams, Fields, Choice } from './types';
|
||||
|
||||
const useGetChoicesFields = ['category', 'subcategory', 'priority'];
|
||||
const defaultFields: Fields = {
|
||||
category: [],
|
||||
subcategory: [],
|
||||
priority: [],
|
||||
};
|
||||
|
||||
const choicesToEuiOptions = (choices: Choice[]): EuiSelectOption[] =>
|
||||
choices.map((choice) => ({ value: choice.value, text: choice.label }));
|
||||
|
||||
const ServiceNowSIRParamsFields: React.FunctionComponent<
|
||||
ActionParamsProps<ServiceNowSIRActionParams>
|
||||
> = ({ actionConnector, actionParams, editAction, index, errors, messageVariables }) => {
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
const actionConnectorRef = useRef(actionConnector?.id ?? '');
|
||||
const { incident, comments } = useMemo(
|
||||
() =>
|
||||
actionParams.subActionParams ??
|
||||
(({
|
||||
incident: {},
|
||||
comments: [],
|
||||
} as unknown) as ServiceNowSIRActionParams['subActionParams']),
|
||||
[actionParams.subActionParams]
|
||||
);
|
||||
|
||||
const [choices, setChoices] = useState<Fields>(defaultFields);
|
||||
|
||||
const editSubActionProperty = useCallback(
|
||||
(key: string, value: any) => {
|
||||
const newProps =
|
||||
key !== 'comments'
|
||||
? {
|
||||
incident: { ...incident, [key]: value },
|
||||
comments,
|
||||
}
|
||||
: { incident, [key]: value };
|
||||
editAction('subActionParams', newProps, index);
|
||||
},
|
||||
[comments, editAction, incident, index]
|
||||
);
|
||||
|
||||
const editComment = useCallback(
|
||||
(key, value) => {
|
||||
if (value.length > 0) {
|
||||
editSubActionProperty(key, [{ commentId: '1', comment: value }]);
|
||||
}
|
||||
},
|
||||
[editSubActionProperty]
|
||||
);
|
||||
|
||||
const onChoicesSuccess = useCallback((values: Choice[]) => {
|
||||
setChoices(
|
||||
values.reduce(
|
||||
(acc, value) => ({
|
||||
...acc,
|
||||
[value.element]: [...(acc[value.element] != null ? acc[value.element] : []), value],
|
||||
}),
|
||||
defaultFields
|
||||
)
|
||||
);
|
||||
}, []);
|
||||
|
||||
const { isLoading: isLoadingChoices } = useGetChoices({
|
||||
http,
|
||||
toastNotifications: toasts,
|
||||
actionConnector,
|
||||
// Not having a memoized fields variable will cause infinitive API calls.
|
||||
fields: useGetChoicesFields,
|
||||
onSuccess: onChoicesSuccess,
|
||||
});
|
||||
|
||||
const categoryOptions = useMemo(() => choicesToEuiOptions(choices.category), [choices.category]);
|
||||
const priorityOptions = useMemo(() => choicesToEuiOptions(choices.priority), [choices.priority]);
|
||||
|
||||
const subcategoryOptions = useMemo(
|
||||
() =>
|
||||
choicesToEuiOptions(
|
||||
choices.subcategory.filter(
|
||||
(subcategory) => subcategory.dependent_value === incident.category
|
||||
)
|
||||
),
|
||||
[choices.subcategory, incident.category]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (actionConnector != null && actionConnectorRef.current !== actionConnector.id) {
|
||||
actionConnectorRef.current = actionConnector.id;
|
||||
editAction(
|
||||
'subActionParams',
|
||||
{
|
||||
incident: {},
|
||||
comments: [],
|
||||
},
|
||||
index
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [actionConnector]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!actionParams.subAction) {
|
||||
editAction('subAction', 'pushToService', index);
|
||||
}
|
||||
if (!actionParams.subActionParams) {
|
||||
editAction(
|
||||
'subActionParams',
|
||||
{
|
||||
incident: {},
|
||||
comments: [],
|
||||
},
|
||||
index
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [actionParams]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiTitle size="s">
|
||||
<h3>{i18n.INCIDENT}</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
error={errors['subActionParams.incident.short_description']}
|
||||
isInvalid={
|
||||
errors['subActionParams.incident.short_description'].length > 0 &&
|
||||
incident.short_description !== undefined
|
||||
}
|
||||
label={i18n.SHORT_DESCRIPTION_LABEL}
|
||||
>
|
||||
<TextFieldWithMessageVariables
|
||||
index={index}
|
||||
editAction={editSubActionProperty}
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'short_description'}
|
||||
inputTargetValue={incident?.short_description ?? undefined}
|
||||
errors={errors['subActionParams.incident.short_description'] as string[]}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow fullWidth label={i18n.SOURCE_IP_LABEL}>
|
||||
<TextFieldWithMessageVariables
|
||||
index={index}
|
||||
editAction={editSubActionProperty}
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'source_ip'}
|
||||
inputTargetValue={incident?.source_ip ?? undefined}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow fullWidth label={i18n.DEST_IP_LABEL}>
|
||||
<TextFieldWithMessageVariables
|
||||
index={index}
|
||||
editAction={editSubActionProperty}
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'dest_ip'}
|
||||
inputTargetValue={incident?.dest_ip ?? undefined}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow fullWidth label={i18n.MALWARE_URL_LABEL}>
|
||||
<TextFieldWithMessageVariables
|
||||
index={index}
|
||||
editAction={editSubActionProperty}
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'malware_url'}
|
||||
inputTargetValue={incident?.malware_url ?? undefined}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow fullWidth label={i18n.MALWARE_HASH_LABEL}>
|
||||
<TextFieldWithMessageVariables
|
||||
index={index}
|
||||
editAction={editSubActionProperty}
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'malware_hash'}
|
||||
inputTargetValue={incident?.malware_hash ?? undefined}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow fullWidth label={i18n.PRIORITY_LABEL}>
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
data-test-subj="prioritySelect"
|
||||
hasNoInitialSelection
|
||||
isLoading={isLoadingChoices}
|
||||
disabled={isLoadingChoices}
|
||||
options={priorityOptions}
|
||||
value={incident.priority ?? undefined}
|
||||
onChange={(e) => {
|
||||
editAction(
|
||||
'subActionParams',
|
||||
{
|
||||
incident: { ...incident, priority: e.target.value },
|
||||
comments,
|
||||
},
|
||||
index
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow fullWidth label={i18n.CATEGORY_LABEL}>
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
data-test-subj="categorySelect"
|
||||
hasNoInitialSelection
|
||||
isLoading={isLoadingChoices}
|
||||
disabled={isLoadingChoices}
|
||||
options={categoryOptions}
|
||||
value={incident.category ?? undefined}
|
||||
onChange={(e) => {
|
||||
editAction(
|
||||
'subActionParams',
|
||||
{
|
||||
incident: { ...incident, category: e.target.value, subcategory: null },
|
||||
comments,
|
||||
},
|
||||
index
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow fullWidth label={i18n.SUBCATEGORY_LABEL}>
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
data-test-subj="subcategorySelect"
|
||||
hasNoInitialSelection
|
||||
isLoading={isLoadingChoices}
|
||||
disabled={isLoadingChoices}
|
||||
options={subcategoryOptions}
|
||||
// Needs an empty string instead of undefined to select the blank option when changing categories
|
||||
value={incident.subcategory ?? ''}
|
||||
onChange={(e) => editSubActionProperty('subcategory', e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<TextAreaWithMessageVariables
|
||||
index={index}
|
||||
editAction={editSubActionProperty}
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'description'}
|
||||
inputTargetValue={incident.description ?? undefined}
|
||||
label={i18n.DESCRIPTION_LABEL}
|
||||
/>
|
||||
<TextAreaWithMessageVariables
|
||||
index={index}
|
||||
editAction={editComment}
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'comments'}
|
||||
inputTargetValue={comments && comments.length > 0 ? comments[0].comment : undefined}
|
||||
label={i18n.COMMENTS_LABEL}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { ServiceNowSIRParamsFields as default };
|
|
@ -6,17 +6,31 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const SERVICENOW_DESC = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.selectMessageText',
|
||||
export const SERVICENOW_ITSM_DESC = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.serviceNowITSM.selectMessageText',
|
||||
{
|
||||
defaultMessage: 'Create an incident in ServiceNow.',
|
||||
defaultMessage: 'Create an incident in ServiceNow ITSM.',
|
||||
}
|
||||
);
|
||||
|
||||
export const SERVICENOW_TITLE = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.actionTypeTitle',
|
||||
export const SERVICENOW_SIR_DESC = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.serviceNowSIR.selectMessageText',
|
||||
{
|
||||
defaultMessage: 'ServiceNow',
|
||||
defaultMessage: 'Create an incident in ServiceNow SIR.',
|
||||
}
|
||||
);
|
||||
|
||||
export const SERVICENOW_ITSM_TITLE = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.serviceNowITSM.actionTypeTitle',
|
||||
{
|
||||
defaultMessage: 'ServiceNow ITSM',
|
||||
}
|
||||
);
|
||||
|
||||
export const SERVICENOW_SIR_TITLE = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.serviceNowSIR.actionTypeTitle',
|
||||
{
|
||||
defaultMessage: 'ServiceNow SIR',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -98,65 +112,114 @@ export const PASSWORD_REQUIRED = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const API_TOKEN_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiTokenTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Api token',
|
||||
}
|
||||
);
|
||||
|
||||
export const API_TOKEN_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredApiTokenTextField',
|
||||
{
|
||||
defaultMessage: 'Api token is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const EMAIL_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.emailTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Email',
|
||||
}
|
||||
);
|
||||
|
||||
export const EMAIL_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredEmailTextField',
|
||||
{
|
||||
defaultMessage: 'Email is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const MAPPING_FIELD_SHORT_DESC = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.mappingFieldShortDescription',
|
||||
{
|
||||
defaultMessage: 'Short Description',
|
||||
}
|
||||
);
|
||||
|
||||
export const MAPPING_FIELD_DESC = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.mappingFieldDescription',
|
||||
{
|
||||
defaultMessage: 'Description',
|
||||
}
|
||||
);
|
||||
|
||||
export const MAPPING_FIELD_COMMENTS = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.mappingFieldComments',
|
||||
{
|
||||
defaultMessage: 'Comments',
|
||||
}
|
||||
);
|
||||
|
||||
export const DESCRIPTION_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.common.requiredDescriptionTextField',
|
||||
{
|
||||
defaultMessage: 'Description is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const TITLE_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.common.requiredShortDescTextField',
|
||||
{
|
||||
defaultMessage: 'Short description is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const SOURCE_IP_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.sourceIPTitle',
|
||||
{
|
||||
defaultMessage: 'Source IP',
|
||||
}
|
||||
);
|
||||
|
||||
export const DEST_IP_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.destinationIPTitle',
|
||||
{
|
||||
defaultMessage: 'Destination IP',
|
||||
}
|
||||
);
|
||||
|
||||
export const INCIDENT = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.title',
|
||||
{
|
||||
defaultMessage: 'Incident',
|
||||
}
|
||||
);
|
||||
|
||||
export const SHORT_DESCRIPTION_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.titleFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Short description (required)',
|
||||
}
|
||||
);
|
||||
|
||||
export const DESCRIPTION_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.descriptionTextAreaFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Description',
|
||||
}
|
||||
);
|
||||
|
||||
export const COMMENTS_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.commentsTextAreaFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Additional comments',
|
||||
}
|
||||
);
|
||||
|
||||
export const MALWARE_URL_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.malwareURLTitle',
|
||||
{
|
||||
defaultMessage: 'Malware URL',
|
||||
}
|
||||
);
|
||||
|
||||
export const MALWARE_HASH_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.malwareHashTitle',
|
||||
{
|
||||
defaultMessage: 'Malware hash',
|
||||
}
|
||||
);
|
||||
|
||||
export const CHOICES_API_ERROR = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.unableToGetChoicesMessage',
|
||||
{
|
||||
defaultMessage: 'Unable to get choices',
|
||||
}
|
||||
);
|
||||
|
||||
export const CATEGORY_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.categoryTitle',
|
||||
{
|
||||
defaultMessage: 'Category',
|
||||
}
|
||||
);
|
||||
|
||||
export const SUBCATEGORY_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.subcategoryTitle',
|
||||
{
|
||||
defaultMessage: 'Subcategory',
|
||||
}
|
||||
);
|
||||
|
||||
export const URGENCY_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.urgencySelectFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Urgency',
|
||||
}
|
||||
);
|
||||
|
||||
export const SEVERITY_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Severity',
|
||||
}
|
||||
);
|
||||
|
||||
export const IMPACT_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.impactSelectFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Impact',
|
||||
}
|
||||
);
|
||||
|
||||
export const PRIORITY_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.prioritySelectFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Priority',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -4,18 +4,27 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiSelectOption } from '@elastic/eui';
|
||||
import { UserConfiguredActionConnector } from '../../../../types';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { ExecutorSubActionPushParams } from '../../../../../../actions/server/builtin_action_types/servicenow/types';
|
||||
import {
|
||||
ExecutorSubActionPushParamsITSM,
|
||||
ExecutorSubActionPushParamsSIR,
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
} from '../../../../../../actions/server/builtin_action_types/servicenow/types';
|
||||
|
||||
export type ServiceNowActionConnector = UserConfiguredActionConnector<
|
||||
ServiceNowConfig,
|
||||
ServiceNowSecrets
|
||||
>;
|
||||
|
||||
export interface ServiceNowActionParams {
|
||||
export interface ServiceNowITSMActionParams {
|
||||
subAction: string;
|
||||
subActionParams: ExecutorSubActionPushParams;
|
||||
subActionParams: ExecutorSubActionPushParamsITSM;
|
||||
}
|
||||
|
||||
export interface ServiceNowSIRActionParams {
|
||||
subAction: string;
|
||||
subActionParams: ExecutorSubActionPushParamsSIR;
|
||||
}
|
||||
|
||||
export interface ServiceNowConfig {
|
||||
|
@ -26,3 +35,13 @@ export interface ServiceNowSecrets {
|
|||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface Choice {
|
||||
value: string;
|
||||
label: string;
|
||||
element: string;
|
||||
dependent_value: string;
|
||||
}
|
||||
|
||||
export type Fields = Record<string, Choice[]>;
|
||||
export type Options = Record<string, EuiSelectOption[]>;
|
||||
|
|
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
* 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 { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { ActionConnector } from '../../../../types';
|
||||
import { useGetChoices, UseGetChoices, UseGetChoicesProps } from './use_get_choices';
|
||||
import { getChoices } from './api';
|
||||
|
||||
jest.mock('./api');
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
|
||||
const getChoicesMock = getChoices as jest.Mock;
|
||||
const onSuccess = jest.fn();
|
||||
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.servicenow',
|
||||
name: 'ServiceNow ITSM',
|
||||
isPreconfigured: false,
|
||||
config: {
|
||||
apiUrl: 'https://dev94428.service-now.com/',
|
||||
},
|
||||
} as ActionConnector;
|
||||
|
||||
const getChoicesResponse = [
|
||||
{
|
||||
dependent_value: '',
|
||||
label: 'Priviledge Escalation',
|
||||
value: 'Priviledge Escalation',
|
||||
element: 'category',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: 'Criminal activity/investigation',
|
||||
value: 'Criminal activity/investigation',
|
||||
element: 'category',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: 'Denial of Service',
|
||||
value: 'Denial of Service',
|
||||
element: 'category',
|
||||
},
|
||||
];
|
||||
|
||||
describe('useGetChoices', () => {
|
||||
const { services } = useKibanaMock();
|
||||
getChoicesMock.mockResolvedValue({
|
||||
data: getChoicesResponse,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const fields = ['priority'];
|
||||
|
||||
it('init', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<UseGetChoicesProps, UseGetChoices>(() =>
|
||||
useGetChoices({
|
||||
http: services.http,
|
||||
actionConnector,
|
||||
toastNotifications: services.notifications.toasts,
|
||||
fields,
|
||||
onSuccess,
|
||||
})
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toEqual({
|
||||
isLoading: false,
|
||||
choices: getChoicesResponse,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an empty array when connector is not presented', async () => {
|
||||
const { result } = renderHook<UseGetChoicesProps, UseGetChoices>(() =>
|
||||
useGetChoices({
|
||||
http: services.http,
|
||||
actionConnector: undefined,
|
||||
toastNotifications: services.notifications.toasts,
|
||||
fields,
|
||||
onSuccess,
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.current).toEqual({
|
||||
isLoading: false,
|
||||
choices: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('it calls onSuccess', async () => {
|
||||
const { waitForNextUpdate } = renderHook<UseGetChoicesProps, UseGetChoices>(() =>
|
||||
useGetChoices({
|
||||
http: services.http,
|
||||
actionConnector,
|
||||
toastNotifications: services.notifications.toasts,
|
||||
fields,
|
||||
onSuccess,
|
||||
})
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(onSuccess).toHaveBeenCalledWith(getChoicesResponse);
|
||||
});
|
||||
|
||||
it('it displays an error when service fails', async () => {
|
||||
getChoicesMock.mockResolvedValue({
|
||||
status: 'error',
|
||||
serviceMessage: 'An error occurred',
|
||||
});
|
||||
|
||||
const { waitForNextUpdate } = renderHook<UseGetChoicesProps, UseGetChoices>(() =>
|
||||
useGetChoices({
|
||||
http: services.http,
|
||||
actionConnector,
|
||||
toastNotifications: services.notifications.toasts,
|
||||
fields,
|
||||
onSuccess,
|
||||
})
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({
|
||||
text: 'An error occurred',
|
||||
title: 'Unable to get choices',
|
||||
});
|
||||
});
|
||||
|
||||
it('it displays an error when http throws an error', async () => {
|
||||
getChoicesMock.mockImplementation(() => {
|
||||
throw new Error('An error occurred');
|
||||
});
|
||||
|
||||
renderHook<UseGetChoicesProps, UseGetChoices>(() =>
|
||||
useGetChoices({
|
||||
http: services.http,
|
||||
actionConnector,
|
||||
toastNotifications: services.notifications.toasts,
|
||||
fields,
|
||||
onSuccess,
|
||||
})
|
||||
);
|
||||
|
||||
expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({
|
||||
text: 'An error occurred',
|
||||
title: 'Unable to get choices',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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 { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { HttpSetup, ToastsApi } from 'kibana/public';
|
||||
import { ActionConnector } from '../../../../types';
|
||||
import { getChoices } from './api';
|
||||
import { Choice } from './types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export interface UseGetChoicesProps {
|
||||
http: HttpSetup;
|
||||
toastNotifications: Pick<
|
||||
ToastsApi,
|
||||
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
|
||||
>;
|
||||
actionConnector?: ActionConnector;
|
||||
fields: string[];
|
||||
onSuccess?: (choices: Choice[]) => void;
|
||||
}
|
||||
|
||||
export interface UseGetChoices {
|
||||
choices: Choice[];
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const useGetChoices = ({
|
||||
http,
|
||||
actionConnector,
|
||||
toastNotifications,
|
||||
fields,
|
||||
onSuccess,
|
||||
}: UseGetChoicesProps): UseGetChoices => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [choices, setChoices] = useState<Choice[]>([]);
|
||||
const didCancel = useRef(false);
|
||||
const abortCtrl = useRef(new AbortController());
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
if (!actionConnector) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
didCancel.current = false;
|
||||
abortCtrl.current.abort();
|
||||
abortCtrl.current = new AbortController();
|
||||
setIsLoading(true);
|
||||
|
||||
const res = await getChoices({
|
||||
http,
|
||||
signal: abortCtrl.current.signal,
|
||||
connectorId: actionConnector.id,
|
||||
fields,
|
||||
});
|
||||
|
||||
if (!didCancel.current) {
|
||||
setIsLoading(false);
|
||||
setChoices(res.data ?? []);
|
||||
if (res.status && res.status === 'error') {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.CHOICES_API_ERROR,
|
||||
text: `${res.serviceMessage ?? res.message}`,
|
||||
});
|
||||
} else if (onSuccess) {
|
||||
onSuccess(res.data ?? []);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (!didCancel.current) {
|
||||
setIsLoading(false);
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.CHOICES_API_ERROR,
|
||||
text: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [actionConnector, http, fields, onSuccess, toastNotifications]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
|
||||
return () => {
|
||||
didCancel.current = true;
|
||||
abortCtrl.current.abort();
|
||||
setIsLoading(false);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return {
|
||||
choices,
|
||||
isLoading,
|
||||
};
|
||||
};
|
|
@ -34,7 +34,11 @@ import { ActionTypeForm, ActionTypeFormProps } from './action_type_form';
|
|||
import { AddConnectorInline } from './connector_add_inline';
|
||||
import { actionTypeCompare } from '../../lib/action_type_compare';
|
||||
import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled';
|
||||
import { VIEW_LICENSE_OPTIONS_LINK, DEFAULT_HIDDEN_ACTION_TYPES } from '../../../common/constants';
|
||||
import {
|
||||
VIEW_LICENSE_OPTIONS_LINK,
|
||||
DEFAULT_HIDDEN_ACTION_TYPES,
|
||||
DEFAULT_HIDDEN_ONLY_ON_ALERTS_ACTION_TYPES,
|
||||
} from '../../../common/constants';
|
||||
import { ActionGroup, AlertActionParam } from '../../../../../alerts/common';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { DefaultActionParamsGetter } from '../../lib/get_defaults_for_action_params';
|
||||
|
@ -230,9 +234,15 @@ export const ActionForm = ({
|
|||
.list()
|
||||
/**
|
||||
* TODO: Remove when cases connector is available across Kibana. Issue: https://github.com/elastic/kibana/issues/82502.
|
||||
* TODO: Need to decide about ServiceNow SIR connector.
|
||||
* If actionTypes are set, hidden connectors are filtered out. Otherwise, they are not.
|
||||
*/
|
||||
.filter(({ id }) => actionTypes ?? !DEFAULT_HIDDEN_ACTION_TYPES.includes(id))
|
||||
.filter(
|
||||
({ id }) =>
|
||||
actionTypes ??
|
||||
(!DEFAULT_HIDDEN_ACTION_TYPES.includes(id) &&
|
||||
!DEFAULT_HIDDEN_ONLY_ON_ALERTS_ACTION_TYPES.includes(id))
|
||||
)
|
||||
.filter((item) => actionTypesIndex[item.id])
|
||||
.filter((item) => !!item.actionParamsFields)
|
||||
.sort((a, b) =>
|
||||
|
|
|
@ -11,3 +11,5 @@ export { builtInGroupByTypes } from './group_by_types';
|
|||
export const VIEW_LICENSE_OPTIONS_LINK = 'https://www.elastic.co/subscriptions';
|
||||
// TODO: Remove when cases connector is available across Kibana. Issue: https://github.com/elastic/kibana/issues/82502.
|
||||
export const DEFAULT_HIDDEN_ACTION_TYPES = ['.case'];
|
||||
// Action types included in this array will be hidden only from the alert's action type node list
|
||||
export const DEFAULT_HIDDEN_ONLY_ON_ALERTS_ACTION_TYPES = ['.servicenow-sir'];
|
||||
|
|
|
@ -10,6 +10,6 @@ export * from './index_controls';
|
|||
export * from './lib';
|
||||
export * from './types';
|
||||
|
||||
export { connectorConfiguration as ServiceNowConnectorConfiguration } from '../application/components/builtin_action_types/servicenow/config';
|
||||
export { serviceNowITSMConfiguration as ServiceNowITSMConnectorConfiguration } from '../application/components/builtin_action_types/servicenow/config';
|
||||
export { connectorConfiguration as JiraConnectorConfiguration } from '../application/components/builtin_action_types/jira/config';
|
||||
export { connectorConfiguration as ResilientConnectorConfiguration } from '../application/components/builtin_action_types/resilient/config';
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
JiraActionTypeId,
|
||||
PagerDutyActionTypeId,
|
||||
ServerLogActionTypeId,
|
||||
ServiceNowActionTypeId,
|
||||
ServiceNowITSMActionTypeId as ServiceNowActionTypeId,
|
||||
SlackActionTypeId,
|
||||
TeamsActionTypeId,
|
||||
WebhookActionTypeId,
|
||||
|
|
|
@ -38,6 +38,7 @@ export function getAllExternalServiceSimulatorPaths(): string[] {
|
|||
getExternalServiceSimulatorPath(service)
|
||||
);
|
||||
allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v2/table/incident`);
|
||||
allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v2/table/sys_choice`);
|
||||
allPaths.push(
|
||||
`/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v2/table/sys_dictionary`
|
||||
);
|
||||
|
|
|
@ -127,6 +127,51 @@ export function initPlugin(router: IRouter, path: string) {
|
|||
});
|
||||
}
|
||||
);
|
||||
|
||||
router.get(
|
||||
{
|
||||
path: `${path}/api/now/v2/table/sys_choice`,
|
||||
options: {
|
||||
authRequired: false,
|
||||
},
|
||||
validate: {},
|
||||
},
|
||||
async function (
|
||||
context: RequestHandlerContext,
|
||||
req: KibanaRequest<any, any, any, any>,
|
||||
res: KibanaResponseFactory
|
||||
): Promise<IKibanaResponse<any>> {
|
||||
return jsonResponse(res, 200, {
|
||||
result: [
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '1 - Critical',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '2 - High',
|
||||
value: '2',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '3 - Moderate',
|
||||
value: '3',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '4 - Low',
|
||||
value: '4',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '5 - Planning',
|
||||
value: '5',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function jsonResponse(res: KibanaResponseFactory, code: number, object?: Record<string, unknown>) {
|
||||
|
|
|
@ -216,7 +216,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) {
|
|||
// Cannot destructure property 'value' of 'undefined' as it is undefined.
|
||||
//
|
||||
// The error seems to come from the exact same place in the code based on the
|
||||
// exact same circomstances:
|
||||
// exact same circumstances:
|
||||
//
|
||||
// https://github.com/elastic/kibana/blob/b0a223ebcbac7e404e8ae6da23b2cc6a4b509ff1/packages/kbn-config-schema/src/types/literal_type.ts#L28
|
||||
//
|
||||
|
@ -247,7 +247,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) {
|
|||
status: 'error',
|
||||
retry: false,
|
||||
message:
|
||||
'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subAction]: expected value to equal [pushToService]',
|
||||
'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subAction]: expected value to equal [pushToService]\n- [4.subAction]: expected value to equal [getChoices]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -265,7 +265,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) {
|
|||
status: 'error',
|
||||
retry: false,
|
||||
message:
|
||||
'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.incident.short_description]: expected value of type [string] but got [undefined]',
|
||||
'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.incident.short_description]: expected value of type [string] but got [undefined]\n- [4.subAction]: expected value to equal [getChoices]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -288,7 +288,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) {
|
|||
status: 'error',
|
||||
retry: false,
|
||||
message:
|
||||
'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.incident.short_description]: expected value of type [string] but got [undefined]',
|
||||
'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.incident.short_description]: expected value of type [string] but got [undefined]\n- [4.subAction]: expected value to equal [getChoices]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -315,7 +315,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) {
|
|||
status: 'error',
|
||||
retry: false,
|
||||
message:
|
||||
'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.commentId]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]',
|
||||
'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.commentId]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]\n- [4.subAction]: expected value to equal [getChoices]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -342,10 +342,33 @@ export default function servicenowTest({ getService }: FtrProviderContext) {
|
|||
status: 'error',
|
||||
retry: false,
|
||||
message:
|
||||
'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.comment]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]',
|
||||
'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.comment]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]\n- [4.subAction]: expected value to equal [getChoices]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getChoices', () => {
|
||||
it('should fail when field is not provided', async () => {
|
||||
await supertest
|
||||
.post(`/api/actions/action/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
subAction: 'getChoices',
|
||||
subActionParams: {},
|
||||
},
|
||||
})
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
actionId: simulatedActionId,
|
||||
status: 'error',
|
||||
retry: false,
|
||||
message:
|
||||
'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subAction]: expected value to equal [pushToService]\n- [4.subActionParams.fields]: expected value of type [array] but got [undefined]',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Execution', () => {
|
||||
|
@ -376,6 +399,54 @@ export default function servicenowTest({ getService }: FtrProviderContext) {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('getChoices', () => {
|
||||
it('should get choices', async () => {
|
||||
const { body: result } = await supertest
|
||||
.post(`/api/actions/action/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
subAction: 'getChoices',
|
||||
subActionParams: { fields: ['priority'] },
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(proxyHaveBeenCalled).to.equal(true);
|
||||
expect(result).to.eql({
|
||||
status: 'ok',
|
||||
actionId: simulatedActionId,
|
||||
data: [
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '1 - Critical',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '2 - High',
|
||||
value: '2',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '3 - Moderate',
|
||||
value: '3',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '4 - Low',
|
||||
value: '4',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '5 - Planning',
|
||||
value: '5',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue