[Alerting][Connectors] Refactor IBM Resilient: Generic Implementation (phase one) (#74357)
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
177d67e8bc
commit
22b4e40ea0
|
@ -331,15 +331,17 @@ 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 |
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
|
@ -561,8 +563,8 @@ The ServiceNow action uses the [V2 Table API](https://developer.servicenow.com/a
|
|||
| Property | Description | Type |
|
||||
| ------------- | ------------------------------------------------------------------------------------------------------------------------- | --------------------- |
|
||||
| savedObjectId | The id of the saved object. | string |
|
||||
| title | The title of the case. | string _(optional)_ |
|
||||
| description | The description of the case. | string _(optional)_ |
|
||||
| title | The title of the incident. | string _(optional)_ |
|
||||
| description | The description of the incident. | string _(optional)_ |
|
||||
| comment | A comment. | string _(optional)_ |
|
||||
| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ |
|
||||
| externalId | The id of the incident in ServiceNow. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ |
|
||||
|
@ -601,16 +603,16 @@ The Jira action uses the [V2 API](https://developer.atlassian.com/cloud/jira/pla
|
|||
|
||||
#### `subActionParams (pushToService)`
|
||||
|
||||
| Property | Description | Type |
|
||||
| ------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------- |
|
||||
| savedObjectId | The id of the saved object | string |
|
||||
| title | The title of the case | string _(optional)_ |
|
||||
| description | The description of the case | string _(optional)_ |
|
||||
| externalId | The id of the incident in Jira. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ |
|
||||
| issueType | The id of the issue type in Jira. | string _(optional)_ |
|
||||
| priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ |
|
||||
| labels | An array of labels. | string[] _(optional)_ |
|
||||
| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ |
|
||||
| Property | Description | Type |
|
||||
| ------------- | ---------------------------------------------------------------------------------------------------------------- | --------------------- |
|
||||
| savedObjectId | The id of the saved object | string |
|
||||
| title | The title of the issue | string _(optional)_ |
|
||||
| description | The description of the issue | string _(optional)_ |
|
||||
| externalId | The id of the issue in Jira. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ |
|
||||
| issueType | The id of the issue type in Jira. | string _(optional)_ |
|
||||
| priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ |
|
||||
| labels | An array of labels. | string[] _(optional)_ |
|
||||
| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ |
|
||||
|
||||
#### `subActionParams (issueTypes)`
|
||||
|
||||
|
@ -628,10 +630,10 @@ ID: `.resilient`
|
|||
|
||||
### `config`
|
||||
|
||||
| Property | Description | Type |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------ |
|
||||
| apiUrl | IBM Resilient instance URL. | string |
|
||||
| incidentConfiguration | Case configuration object. The object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the Jira field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'summary', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in IBM Resilient and will be overwrite on each update. | object |
|
||||
| Property | Description | Type |
|
||||
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ |
|
||||
| apiUrl | IBM Resilient instance URL. | string |
|
||||
| incidentConfiguration | Optional property and specific to **Cases only**. If defined, the object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the Jira field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'summary', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in IBM Resilient and will be overwrite on each update. | object |
|
||||
|
||||
### `secrets`
|
||||
|
||||
|
@ -652,10 +654,12 @@ ID: `.resilient`
|
|||
| Property | Description | Type |
|
||||
| ------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------- |
|
||||
| savedObjectId | The id of the saved object | string |
|
||||
| title | The title of the case | string _(optional)_ |
|
||||
| description | The description of the case | string _(optional)_ |
|
||||
| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ |
|
||||
| title | The title of the incident | string _(optional)_ |
|
||||
| description | The description of the incident | string _(optional)_ |
|
||||
| comments | The comments of the incident. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ |
|
||||
| externalId | The id of the incident in IBM Resilient. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ |
|
||||
| incidentTypes | An array with the ids of IBM Resilient incident types. | number[] _(optional)_ |
|
||||
| severityCode | IBM Resilient id of the severity code. | number _(optional)_ |
|
||||
|
||||
# Command Line Utility
|
||||
|
||||
|
|
|
@ -1,93 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
ExternalServiceApi,
|
||||
ExternalServiceParams,
|
||||
PushToServiceResponse,
|
||||
GetIncidentApiHandlerArgs,
|
||||
HandshakeApiHandlerArgs,
|
||||
PushToServiceApiHandlerArgs,
|
||||
} from './types';
|
||||
import { prepareFieldsForTransformation, transformFields, transformComments } from './utils';
|
||||
|
||||
const handshakeHandler = async ({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
}: HandshakeApiHandlerArgs) => {};
|
||||
const getIncidentHandler = async ({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
}: GetIncidentApiHandlerArgs) => {};
|
||||
|
||||
const pushToServiceHandler = async ({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
}: PushToServiceApiHandlerArgs): Promise<PushToServiceResponse> => {
|
||||
const { externalId, comments } = params;
|
||||
const updateIncident = externalId ? true : false;
|
||||
const defaultPipes = updateIncident ? ['informationUpdated'] : ['informationCreated'];
|
||||
let currentIncident: ExternalServiceParams | undefined;
|
||||
let res: PushToServiceResponse;
|
||||
|
||||
if (externalId) {
|
||||
currentIncident = await externalService.getIncident(externalId);
|
||||
}
|
||||
|
||||
const fields = prepareFieldsForTransformation({
|
||||
externalCase: params.externalCase,
|
||||
mapping,
|
||||
defaultPipes,
|
||||
});
|
||||
|
||||
const incident = transformFields({
|
||||
params,
|
||||
fields,
|
||||
currentIncident,
|
||||
});
|
||||
|
||||
if (updateIncident) {
|
||||
res = await externalService.updateIncident({ incidentId: externalId, incident });
|
||||
} else {
|
||||
res = await externalService.createIncident({ incident });
|
||||
}
|
||||
|
||||
if (
|
||||
comments &&
|
||||
Array.isArray(comments) &&
|
||||
comments.length > 0 &&
|
||||
mapping.get('comments')?.actionType !== 'nothing'
|
||||
) {
|
||||
const commentsTransformed = transformComments(comments, ['informationAdded']);
|
||||
|
||||
res.comments = [];
|
||||
for (const currentComment of commentsTransformed) {
|
||||
const comment = await externalService.createComment({
|
||||
incidentId: res.id,
|
||||
comment: currentComment,
|
||||
field: mapping.get('comments')?.target ?? 'comments',
|
||||
});
|
||||
res.comments = [
|
||||
...(res.comments ?? []),
|
||||
{
|
||||
commentId: comment.commentId,
|
||||
pushedDate: comment.pushedDate,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const api: ExternalServiceApi = {
|
||||
handshake: handshakeHandler,
|
||||
pushToService: pushToServiceHandler,
|
||||
getIncident: getIncidentHandler,
|
||||
};
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
export const MappingActionType = schema.oneOf([
|
||||
schema.literal('nothing'),
|
||||
schema.literal('overwrite'),
|
||||
schema.literal('append'),
|
||||
]);
|
||||
|
||||
export const MapRecordSchema = schema.object({
|
||||
source: schema.string(),
|
||||
target: schema.string(),
|
||||
actionType: MappingActionType,
|
||||
});
|
||||
|
||||
export const IncidentConfigurationSchema = schema.object({
|
||||
mapping: schema.arrayOf(MapRecordSchema),
|
||||
});
|
||||
|
||||
export const UserSchema = schema.object({
|
||||
fullName: schema.nullable(schema.string()),
|
||||
username: schema.nullable(schema.string()),
|
||||
});
|
||||
|
||||
export const EntityInformation = {
|
||||
createdAt: schema.nullable(schema.string()),
|
||||
createdBy: schema.nullable(UserSchema),
|
||||
updatedAt: schema.nullable(schema.string()),
|
||||
updatedBy: schema.nullable(UserSchema),
|
||||
};
|
||||
|
||||
export const EntityInformationSchema = schema.object(EntityInformation);
|
||||
|
||||
export const CommentSchema = schema.object({
|
||||
commentId: schema.string(),
|
||||
comment: schema.string(),
|
||||
...EntityInformation,
|
||||
});
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import {
|
||||
IncidentConfigurationSchema,
|
||||
MapRecordSchema,
|
||||
CommentSchema,
|
||||
EntityInformationSchema,
|
||||
} from './common_schema';
|
||||
|
||||
export interface CreateCommentRequest {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export type IncidentConfiguration = TypeOf<typeof IncidentConfigurationSchema>;
|
||||
export type MapRecord = TypeOf<typeof MapRecordSchema>;
|
||||
export type Comment = TypeOf<typeof CommentSchema>;
|
||||
export type EntityInformation = TypeOf<typeof EntityInformationSchema>;
|
||||
|
||||
export interface ExternalServiceCommentResponse {
|
||||
commentId: string;
|
||||
pushedDate: string;
|
||||
externalCommentId?: string;
|
||||
}
|
||||
|
||||
export interface PipedField {
|
||||
key: string;
|
||||
value: string;
|
||||
actionType: string;
|
||||
pipes: string[];
|
||||
}
|
||||
|
||||
export interface TransformFieldsArgs<P, S> {
|
||||
params: P;
|
||||
fields: PipedField[];
|
||||
currentIncident?: S;
|
||||
}
|
||||
|
||||
export interface TransformerArgs {
|
||||
value: string;
|
||||
date?: string;
|
||||
user?: string;
|
||||
previousValue?: string;
|
||||
}
|
|
@ -18,36 +18,18 @@ export const MapRecordSchema = schema.object({
|
|||
actionType: MappingActionType,
|
||||
});
|
||||
|
||||
export const CaseConfigurationSchema = schema.object({
|
||||
export const IncidentConfigurationSchema = schema.object({
|
||||
mapping: schema.arrayOf(MapRecordSchema),
|
||||
});
|
||||
|
||||
export const ExternalIncidentServiceConfiguration = {
|
||||
apiUrl: schema.string(),
|
||||
casesConfiguration: CaseConfigurationSchema,
|
||||
};
|
||||
|
||||
export const ExternalIncidentServiceConfigurationSchema = schema.object(
|
||||
ExternalIncidentServiceConfiguration
|
||||
);
|
||||
|
||||
export const ExternalIncidentServiceSecretConfiguration = {
|
||||
password: schema.string(),
|
||||
username: schema.string(),
|
||||
};
|
||||
|
||||
export const ExternalIncidentServiceSecretConfigurationSchema = schema.object(
|
||||
ExternalIncidentServiceSecretConfiguration
|
||||
);
|
||||
|
||||
export const UserSchema = schema.object({
|
||||
fullName: schema.nullable(schema.string()),
|
||||
username: schema.nullable(schema.string()),
|
||||
});
|
||||
|
||||
const EntityInformation = {
|
||||
createdAt: schema.string(),
|
||||
createdBy: UserSchema,
|
||||
export const EntityInformation = {
|
||||
createdAt: schema.nullable(schema.string()),
|
||||
createdBy: schema.nullable(UserSchema),
|
||||
updatedAt: schema.nullable(schema.string()),
|
||||
updatedBy: schema.nullable(UserSchema),
|
||||
};
|
||||
|
@ -59,40 +41,3 @@ export const CommentSchema = schema.object({
|
|||
comment: schema.string(),
|
||||
...EntityInformation,
|
||||
});
|
||||
|
||||
export const ExecutorSubActionSchema = schema.oneOf([
|
||||
schema.literal('getIncident'),
|
||||
schema.literal('pushToService'),
|
||||
schema.literal('handshake'),
|
||||
]);
|
||||
|
||||
export const ExecutorSubActionPushParamsSchema = schema.object({
|
||||
savedObjectId: schema.string(),
|
||||
title: schema.string(),
|
||||
description: schema.nullable(schema.string()),
|
||||
comments: schema.nullable(schema.arrayOf(CommentSchema)),
|
||||
externalId: schema.nullable(schema.string()),
|
||||
...EntityInformation,
|
||||
});
|
||||
|
||||
export const ExecutorSubActionGetIncidentParamsSchema = schema.object({
|
||||
externalId: schema.string(),
|
||||
});
|
||||
|
||||
// Reserved for future implementation
|
||||
export const ExecutorSubActionHandshakeParamsSchema = schema.object({});
|
||||
|
||||
export const ExecutorParamsSchema = schema.oneOf([
|
||||
schema.object({
|
||||
subAction: schema.literal('getIncident'),
|
||||
subActionParams: ExecutorSubActionGetIncidentParamsSchema,
|
||||
}),
|
||||
schema.object({
|
||||
subAction: schema.literal('handshake'),
|
||||
subActionParams: ExecutorSubActionHandshakeParamsSchema,
|
||||
}),
|
||||
schema.object({
|
||||
subAction: schema.literal('pushToService'),
|
||||
subActionParams: ExecutorSubActionPushParamsSchema,
|
||||
}),
|
||||
]);
|
||||
|
|
|
@ -4,74 +4,18 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
// This will have to remain `any` until we can extend connectors with generics
|
||||
// and circular dependencies eliminated.
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
|
||||
import {
|
||||
ExternalIncidentServiceConfigurationSchema,
|
||||
ExternalIncidentServiceSecretConfigurationSchema,
|
||||
ExecutorParamsSchema,
|
||||
CaseConfigurationSchema,
|
||||
IncidentConfigurationSchema,
|
||||
MapRecordSchema,
|
||||
CommentSchema,
|
||||
ExecutorSubActionPushParamsSchema,
|
||||
ExecutorSubActionGetIncidentParamsSchema,
|
||||
ExecutorSubActionHandshakeParamsSchema,
|
||||
EntityInformationSchema,
|
||||
} from './schema';
|
||||
import { LicenseType } from '../../../../../legacy/common/constants';
|
||||
|
||||
export interface AnyParams {
|
||||
[index: string]: string | number | object | undefined | null;
|
||||
}
|
||||
|
||||
export type ExternalIncidentServiceConfiguration = TypeOf<
|
||||
typeof ExternalIncidentServiceConfigurationSchema
|
||||
>;
|
||||
export type ExternalIncidentServiceSecretConfiguration = TypeOf<
|
||||
typeof ExternalIncidentServiceSecretConfigurationSchema
|
||||
>;
|
||||
|
||||
export type ExecutorParams = TypeOf<typeof ExecutorParamsSchema>;
|
||||
export type ExecutorSubActionPushParams = TypeOf<typeof ExecutorSubActionPushParamsSchema>;
|
||||
|
||||
export type ExecutorSubActionGetIncidentParams = TypeOf<
|
||||
typeof ExecutorSubActionGetIncidentParamsSchema
|
||||
>;
|
||||
|
||||
export type ExecutorSubActionHandshakeParams = TypeOf<
|
||||
typeof ExecutorSubActionHandshakeParamsSchema
|
||||
>;
|
||||
|
||||
export type CaseConfiguration = TypeOf<typeof CaseConfigurationSchema>;
|
||||
export type IncidentConfiguration = TypeOf<typeof IncidentConfigurationSchema>;
|
||||
export type MapRecord = TypeOf<typeof MapRecordSchema>;
|
||||
export type Comment = TypeOf<typeof CommentSchema>;
|
||||
|
||||
export interface ExternalServiceConfiguration {
|
||||
id: string;
|
||||
name: string;
|
||||
minimumLicenseRequired: LicenseType;
|
||||
}
|
||||
|
||||
export interface ExternalServiceCredentials {
|
||||
config: Record<string, any>;
|
||||
secrets: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ExternalServiceValidation {
|
||||
config: (configurationUtilities: any, configObject: any) => void;
|
||||
secrets: (configurationUtilities: any, secrets: any) => void;
|
||||
}
|
||||
|
||||
export interface ExternalServiceIncidentResponse {
|
||||
id: string;
|
||||
title: string;
|
||||
url: string;
|
||||
pushedDate: string;
|
||||
}
|
||||
export type EntityInformation = TypeOf<typeof EntityInformationSchema>;
|
||||
|
||||
export interface ExternalServiceCommentResponse {
|
||||
commentId: string;
|
||||
|
@ -79,69 +23,6 @@ export interface ExternalServiceCommentResponse {
|
|||
externalCommentId?: string;
|
||||
}
|
||||
|
||||
export interface ExternalServiceParams {
|
||||
[index: string]: any;
|
||||
}
|
||||
|
||||
export interface ExternalService {
|
||||
getIncident: (id: string) => Promise<any>;
|
||||
createIncident: (params: ExternalServiceParams) => Promise<ExternalServiceIncidentResponse>;
|
||||
updateIncident: (params: ExternalServiceParams) => Promise<ExternalServiceIncidentResponse>;
|
||||
createComment: (params: ExternalServiceParams) => Promise<ExternalServiceCommentResponse>;
|
||||
}
|
||||
|
||||
export interface PushToServiceApiParams extends ExecutorSubActionPushParams {
|
||||
externalCase: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ExternalServiceApiHandlerArgs {
|
||||
externalService: ExternalService;
|
||||
mapping: Map<string, any>;
|
||||
}
|
||||
|
||||
export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs {
|
||||
params: PushToServiceApiParams;
|
||||
}
|
||||
|
||||
export interface GetIncidentApiHandlerArgs extends ExternalServiceApiHandlerArgs {
|
||||
params: ExecutorSubActionGetIncidentParams;
|
||||
}
|
||||
|
||||
export interface HandshakeApiHandlerArgs extends ExternalServiceApiHandlerArgs {
|
||||
params: ExecutorSubActionHandshakeParams;
|
||||
}
|
||||
|
||||
export interface PushToServiceResponse extends ExternalServiceIncidentResponse {
|
||||
comments?: ExternalServiceCommentResponse[];
|
||||
}
|
||||
|
||||
export interface ExternalServiceApi {
|
||||
handshake: (args: HandshakeApiHandlerArgs) => Promise<void>;
|
||||
pushToService: (args: PushToServiceApiHandlerArgs) => Promise<PushToServiceResponse>;
|
||||
getIncident: (args: GetIncidentApiHandlerArgs) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface CreateExternalServiceBasicArgs {
|
||||
api: ExternalServiceApi;
|
||||
createExternalService: (
|
||||
credentials: ExternalServiceCredentials,
|
||||
logger: Logger,
|
||||
proxySettings?: any
|
||||
) => ExternalService;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export interface CreateExternalServiceArgs extends CreateExternalServiceBasicArgs {
|
||||
config: ExternalServiceConfiguration;
|
||||
validate: ExternalServiceValidation;
|
||||
validationSchema: { config: any; secrets: any };
|
||||
}
|
||||
|
||||
export interface CreateActionTypeArgs {
|
||||
configurationUtilities: any;
|
||||
executor?: any;
|
||||
}
|
||||
|
||||
export interface PipedField {
|
||||
key: string;
|
||||
value: string;
|
||||
|
@ -149,16 +30,10 @@ export interface PipedField {
|
|||
pipes: string[];
|
||||
}
|
||||
|
||||
export interface PrepareFieldsForTransformArgs {
|
||||
externalCase: Record<string, any>;
|
||||
mapping: Map<string, MapRecord>;
|
||||
defaultPipes?: string[];
|
||||
}
|
||||
|
||||
export interface TransformFieldsArgs {
|
||||
params: PushToServiceApiParams;
|
||||
export interface TransformFieldsArgs<P, S> {
|
||||
params: P;
|
||||
fields: PipedField[];
|
||||
currentIncident?: ExternalServiceParams;
|
||||
currentIncident?: S;
|
||||
}
|
||||
|
||||
export interface TransformerArgs {
|
||||
|
@ -167,3 +42,13 @@ export interface TransformerArgs {
|
|||
user?: string;
|
||||
previousValue?: string;
|
||||
}
|
||||
|
||||
export interface AnyParams {
|
||||
[index: string]: string | number | object | undefined | null;
|
||||
}
|
||||
|
||||
export interface PrepareFieldsForTransformArgs {
|
||||
externalCase: Record<string, string>;
|
||||
mapping: Map<string, MapRecord>;
|
||||
defaultPipes?: string[];
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import {
|
||||
normalizeMapping,
|
||||
buildMap,
|
||||
|
@ -14,7 +16,23 @@ import {
|
|||
} from './utils';
|
||||
|
||||
import { SUPPORTED_SOURCE_FIELDS } from './constants';
|
||||
import { Comment, MapRecord, PushToServiceApiParams } from './types';
|
||||
import { Comment, MapRecord } from './types';
|
||||
|
||||
interface Entity {
|
||||
createdAt: string | null;
|
||||
createdBy: { fullName: string; username: string } | null;
|
||||
updatedAt: string | null;
|
||||
updatedBy: { fullName: string; username: string } | null;
|
||||
}
|
||||
|
||||
interface PushToServiceApiParams extends Entity {
|
||||
savedObjectId: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
externalId: string | null;
|
||||
externalObject: Record<string, any>;
|
||||
comments: Comment[];
|
||||
}
|
||||
|
||||
const mapping: MapRecord[] = [
|
||||
{ source: 'title', target: 'short_description', actionType: 'overwrite' },
|
||||
|
@ -22,7 +40,6 @@ const mapping: MapRecord[] = [
|
|||
{ source: 'comments', target: 'comments', actionType: 'append' },
|
||||
];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const finalMapping: Map<string, any> = new Map();
|
||||
|
||||
finalMapping.set('title', {
|
||||
|
@ -61,7 +78,7 @@ const fullParams: PushToServiceApiParams = {
|
|||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
externalId: null,
|
||||
externalCase: {
|
||||
externalObject: {
|
||||
short_description: 'a title',
|
||||
description: 'a description',
|
||||
},
|
||||
|
@ -154,7 +171,7 @@ describe('mapParams', () => {
|
|||
describe('prepareFieldsForTransformation', () => {
|
||||
test('prepare fields with defaults', () => {
|
||||
const res = prepareFieldsForTransformation({
|
||||
externalCase: fullParams.externalCase,
|
||||
externalCase: fullParams.externalObject,
|
||||
mapping: finalMapping,
|
||||
});
|
||||
expect(res).toEqual([
|
||||
|
@ -175,7 +192,7 @@ describe('prepareFieldsForTransformation', () => {
|
|||
|
||||
test('prepare fields with default pipes', () => {
|
||||
const res = prepareFieldsForTransformation({
|
||||
externalCase: fullParams.externalCase,
|
||||
externalCase: fullParams.externalObject,
|
||||
mapping: finalMapping,
|
||||
defaultPipes: ['myTestPipe'],
|
||||
});
|
||||
|
@ -199,11 +216,15 @@ describe('prepareFieldsForTransformation', () => {
|
|||
describe('transformFields', () => {
|
||||
test('transform fields for creation correctly', () => {
|
||||
const fields = prepareFieldsForTransformation({
|
||||
externalCase: fullParams.externalCase,
|
||||
externalCase: fullParams.externalObject,
|
||||
mapping: finalMapping,
|
||||
});
|
||||
|
||||
const res = transformFields({
|
||||
const res = transformFields<
|
||||
PushToServiceApiParams,
|
||||
{},
|
||||
{ short_description: string; description: string }
|
||||
>({
|
||||
params: fullParams,
|
||||
fields,
|
||||
});
|
||||
|
@ -216,12 +237,16 @@ describe('transformFields', () => {
|
|||
|
||||
test('transform fields for update correctly', () => {
|
||||
const fields = prepareFieldsForTransformation({
|
||||
externalCase: fullParams.externalCase,
|
||||
externalCase: fullParams.externalObject,
|
||||
mapping: finalMapping,
|
||||
defaultPipes: ['informationUpdated'],
|
||||
});
|
||||
|
||||
const res = transformFields({
|
||||
const res = transformFields<
|
||||
PushToServiceApiParams,
|
||||
{},
|
||||
{ short_description: string; description: string }
|
||||
>({
|
||||
params: {
|
||||
...fullParams,
|
||||
updatedAt: '2020-03-15T08:34:53.450Z',
|
||||
|
@ -245,12 +270,16 @@ describe('transformFields', () => {
|
|||
|
||||
test('add newline character to description', () => {
|
||||
const fields = prepareFieldsForTransformation({
|
||||
externalCase: fullParams.externalCase,
|
||||
externalCase: fullParams.externalObject,
|
||||
mapping: finalMapping,
|
||||
defaultPipes: ['informationUpdated'],
|
||||
});
|
||||
|
||||
const res = transformFields({
|
||||
const res = transformFields<
|
||||
PushToServiceApiParams,
|
||||
{},
|
||||
{ short_description: string; description: string }
|
||||
>({
|
||||
params: fullParams,
|
||||
fields,
|
||||
currentIncident: {
|
||||
|
@ -263,11 +292,15 @@ describe('transformFields', () => {
|
|||
|
||||
test('append username if fullname is undefined when create', () => {
|
||||
const fields = prepareFieldsForTransformation({
|
||||
externalCase: fullParams.externalCase,
|
||||
externalCase: fullParams.externalObject,
|
||||
mapping: finalMapping,
|
||||
});
|
||||
|
||||
const res = transformFields({
|
||||
const res = transformFields<
|
||||
PushToServiceApiParams,
|
||||
{},
|
||||
{ short_description: string; description: string }
|
||||
>({
|
||||
params: {
|
||||
...fullParams,
|
||||
createdBy: { fullName: '', username: 'elastic' },
|
||||
|
@ -283,12 +316,16 @@ describe('transformFields', () => {
|
|||
|
||||
test('append username if fullname is undefined when update', () => {
|
||||
const fields = prepareFieldsForTransformation({
|
||||
externalCase: fullParams.externalCase,
|
||||
externalCase: fullParams.externalObject,
|
||||
mapping: finalMapping,
|
||||
defaultPipes: ['informationUpdated'],
|
||||
});
|
||||
|
||||
const res = transformFields({
|
||||
const res = transformFields<
|
||||
PushToServiceApiParams,
|
||||
{},
|
||||
{ short_description: string; description: string }
|
||||
>({
|
||||
params: {
|
||||
...fullParams,
|
||||
updatedAt: '2020-03-15T08:34:53.450Z',
|
||||
|
|
|
@ -4,30 +4,16 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { curry, flow, get } from 'lodash';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
import { ActionTypeExecutorOptions, ActionTypeExecutorResult, ActionType } from '../../types';
|
||||
|
||||
import { ExecutorParamsSchema } from './schema';
|
||||
import {
|
||||
ExternalIncidentServiceConfiguration,
|
||||
ExternalIncidentServiceSecretConfiguration,
|
||||
} from './types';
|
||||
import { flow, get } from 'lodash';
|
||||
|
||||
import {
|
||||
CreateExternalServiceArgs,
|
||||
CreateActionTypeArgs,
|
||||
ExecutorParams,
|
||||
MapRecord,
|
||||
AnyParams,
|
||||
CreateExternalServiceBasicArgs,
|
||||
PrepareFieldsForTransformArgs,
|
||||
PipedField,
|
||||
TransformFieldsArgs,
|
||||
Comment,
|
||||
ExecutorSubActionPushParams,
|
||||
PushToServiceResponse,
|
||||
EntityInformation,
|
||||
PipedField,
|
||||
AnyParams,
|
||||
PrepareFieldsForTransformArgs,
|
||||
} from './types';
|
||||
|
||||
import { transformers } from './transformers';
|
||||
|
@ -61,92 +47,6 @@ export const mapParams = <T extends {}>(params: T, mapping: Map<string, MapRecor
|
|||
}, {});
|
||||
};
|
||||
|
||||
export const createConnectorExecutor = ({
|
||||
api,
|
||||
createExternalService,
|
||||
logger,
|
||||
}: CreateExternalServiceBasicArgs) => async (
|
||||
execOptions: ActionTypeExecutorOptions<
|
||||
ExternalIncidentServiceConfiguration,
|
||||
ExternalIncidentServiceSecretConfiguration,
|
||||
ExecutorParams
|
||||
>
|
||||
): Promise<ActionTypeExecutorResult<PushToServiceResponse | {}>> => {
|
||||
const { actionId, config, params, secrets } = execOptions;
|
||||
const { subAction, subActionParams } = params;
|
||||
let data = {};
|
||||
|
||||
const res: ActionTypeExecutorResult<void> = {
|
||||
status: 'ok',
|
||||
actionId,
|
||||
};
|
||||
|
||||
const externalService = createExternalService(
|
||||
{
|
||||
config,
|
||||
secrets,
|
||||
},
|
||||
logger,
|
||||
execOptions.proxySettings
|
||||
);
|
||||
|
||||
if (!api[subAction]) {
|
||||
throw new Error('[Action][ExternalService] Unsupported subAction type.');
|
||||
}
|
||||
|
||||
if (subAction !== 'pushToService') {
|
||||
throw new Error('[Action][ExternalService] subAction not implemented.');
|
||||
}
|
||||
|
||||
if (subAction === 'pushToService') {
|
||||
const pushToServiceParams = subActionParams as ExecutorSubActionPushParams;
|
||||
const { comments, externalId, ...restParams } = pushToServiceParams;
|
||||
|
||||
const mapping = buildMap(config.casesConfiguration.mapping);
|
||||
const externalCase = mapParams<ExecutorSubActionPushParams>(
|
||||
restParams as ExecutorSubActionPushParams,
|
||||
mapping
|
||||
);
|
||||
|
||||
data = await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: { ...pushToServiceParams, externalCase },
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...res,
|
||||
data,
|
||||
};
|
||||
};
|
||||
|
||||
export const createConnector = ({
|
||||
api,
|
||||
config,
|
||||
validate,
|
||||
createExternalService,
|
||||
validationSchema,
|
||||
logger,
|
||||
}: CreateExternalServiceArgs) => {
|
||||
return ({
|
||||
configurationUtilities,
|
||||
executor = createConnectorExecutor({ api, createExternalService, logger }),
|
||||
}: CreateActionTypeArgs): ActionType => ({
|
||||
...config,
|
||||
validate: {
|
||||
config: schema.object(validationSchema.config, {
|
||||
validate: curry(validate.config)(configurationUtilities),
|
||||
}),
|
||||
secrets: schema.object(validationSchema.secrets, {
|
||||
validate: curry(validate.secrets)(configurationUtilities),
|
||||
}),
|
||||
params: ExecutorParamsSchema,
|
||||
},
|
||||
executor,
|
||||
});
|
||||
};
|
||||
|
||||
export const prepareFieldsForTransformation = ({
|
||||
externalCase,
|
||||
mapping,
|
||||
|
@ -165,11 +65,15 @@ export const prepareFieldsForTransformation = ({
|
|||
});
|
||||
};
|
||||
|
||||
export const transformFields = ({
|
||||
export const transformFields = <
|
||||
P extends EntityInformation,
|
||||
S extends Record<string, unknown>,
|
||||
R extends {}
|
||||
>({
|
||||
params,
|
||||
fields,
|
||||
currentIncident,
|
||||
}: TransformFieldsArgs): Record<string, string> => {
|
||||
}: TransformFieldsArgs<P, S>): R => {
|
||||
return fields.reduce((prev, cur) => {
|
||||
const transform = flow(...cur.pipes.map((p) => transformers[p]));
|
||||
return {
|
||||
|
@ -177,18 +81,11 @@ export const transformFields = ({
|
|||
[cur.key]: transform({
|
||||
value: cur.value,
|
||||
date: params.updatedAt ?? params.createdAt,
|
||||
user:
|
||||
(params.updatedBy != null
|
||||
? params.updatedBy.fullName
|
||||
? params.updatedBy.fullName
|
||||
: params.updatedBy.username
|
||||
: params.createdBy.fullName
|
||||
? params.createdBy.fullName
|
||||
: params.createdBy.username) ?? '',
|
||||
user: getEntity(params),
|
||||
previousValue: currentIncident ? currentIncident[cur.key] : '',
|
||||
}).value,
|
||||
};
|
||||
}, {});
|
||||
}, {} as R);
|
||||
};
|
||||
|
||||
export const transformComments = (comments: Comment[], pipes: string[]): Comment[] => {
|
||||
|
@ -197,18 +94,18 @@ export const transformComments = (comments: Comment[], pipes: string[]): Comment
|
|||
comment: flow(...pipes.map((p) => transformers[p]))({
|
||||
value: c.comment,
|
||||
date: c.updatedAt ?? c.createdAt,
|
||||
user:
|
||||
(c.updatedBy != null
|
||||
? c.updatedBy.fullName
|
||||
? c.updatedBy.fullName
|
||||
: c.updatedBy.username
|
||||
: c.createdBy.fullName
|
||||
? c.createdBy.fullName
|
||||
: c.createdBy.username) ?? '',
|
||||
user: getEntity(c),
|
||||
}).value,
|
||||
}));
|
||||
};
|
||||
|
||||
export const getErrorMessage = (connector: string, msg: string) => {
|
||||
return `[Action][${connector}]: ${msg}`;
|
||||
};
|
||||
export const getEntity = (entity: EntityInformation): string =>
|
||||
(entity.updatedBy != null
|
||||
? entity.updatedBy.fullName
|
||||
? entity.updatedBy.fullName
|
||||
: entity.updatedBy.username
|
||||
: entity.createdBy != null
|
||||
? entity.createdBy.fullName
|
||||
? entity.createdBy.fullName
|
||||
: entity.createdBy.username
|
||||
: '') ?? '';
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import { ActionsConfigurationUtilities } from '../../actions_config';
|
||||
import {
|
||||
ExternalIncidentServiceConfiguration,
|
||||
ExternalIncidentServiceSecretConfiguration,
|
||||
} from './types';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const validateCommonConfig = (
|
||||
configurationUtilities: ActionsConfigurationUtilities,
|
||||
configObject: ExternalIncidentServiceConfiguration
|
||||
) => {
|
||||
try {
|
||||
if (isEmpty(configObject.casesConfiguration.mapping)) {
|
||||
return i18n.MAPPING_EMPTY;
|
||||
}
|
||||
|
||||
configurationUtilities.ensureUriAllowed(configObject.apiUrl);
|
||||
} catch (allowListError) {
|
||||
return i18n.WHITE_LISTED_ERROR(allowListError.message);
|
||||
}
|
||||
};
|
||||
|
||||
export const validateCommonSecrets = (
|
||||
configurationUtilities: ActionsConfigurationUtilities,
|
||||
secrets: ExternalIncidentServiceSecretConfiguration
|
||||
) => {};
|
|
@ -4,7 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { flow } from 'lodash';
|
||||
import {
|
||||
ExternalServiceParams,
|
||||
PushToServiceApiHandlerArgs,
|
||||
|
@ -15,14 +14,11 @@ import {
|
|||
GetFieldsByIssueTypeHandlerArgs,
|
||||
GetIssueTypesHandlerArgs,
|
||||
PushToServiceApiParams,
|
||||
PushToServiceResponse,
|
||||
} from './types';
|
||||
|
||||
// TODO: to remove, need to support Case
|
||||
import { transformers } from '../case/transformers';
|
||||
import { TransformFieldsArgs, Comment, EntityInformation } from '../case/common_types';
|
||||
|
||||
import { PushToServiceResponse } from './types';
|
||||
import { prepareFieldsForTransformation } from '../case/utils';
|
||||
import { prepareFieldsForTransformation, transformFields, transformComments } from '../case/utils';
|
||||
|
||||
const handshakeHandler = async ({
|
||||
externalService,
|
||||
|
@ -81,7 +77,7 @@ const pushToServiceHandler = async ({
|
|||
defaultPipes,
|
||||
});
|
||||
|
||||
incident = transformFields({
|
||||
incident = transformFields<PushToServiceApiParams, ExternalServiceParams, Incident>({
|
||||
params,
|
||||
fields,
|
||||
currentIncident,
|
||||
|
@ -132,47 +128,6 @@ const pushToServiceHandler = async ({
|
|||
return res;
|
||||
};
|
||||
|
||||
export const transformFields = ({
|
||||
params,
|
||||
fields,
|
||||
currentIncident,
|
||||
}: TransformFieldsArgs<PushToServiceApiParams, ExternalServiceParams>): Incident => {
|
||||
return fields.reduce((prev, cur) => {
|
||||
const transform = flow(...cur.pipes.map((p) => transformers[p]));
|
||||
return {
|
||||
...prev,
|
||||
[cur.key]: transform({
|
||||
value: cur.value,
|
||||
date: params.updatedAt ?? params.createdAt,
|
||||
user: getEntity(params),
|
||||
previousValue: currentIncident ? currentIncident[cur.key] : '',
|
||||
}).value,
|
||||
};
|
||||
}, {} as Incident);
|
||||
};
|
||||
|
||||
export const transformComments = (comments: Comment[], pipes: string[]): Comment[] => {
|
||||
return comments.map((c) => ({
|
||||
...c,
|
||||
comment: flow(...pipes.map((p) => transformers[p]))({
|
||||
value: c.comment,
|
||||
date: c.updatedAt ?? c.createdAt,
|
||||
user: getEntity(c),
|
||||
}).value,
|
||||
}));
|
||||
};
|
||||
|
||||
export const getEntity = (entity: EntityInformation): string =>
|
||||
(entity.updatedBy != null
|
||||
? entity.updatedBy.fullName
|
||||
? entity.updatedBy.fullName
|
||||
: entity.updatedBy.username
|
||||
: entity.createdBy != null
|
||||
? entity.createdBy.fullName
|
||||
? entity.createdBy.fullName
|
||||
: entity.createdBy.username
|
||||
: '') ?? '';
|
||||
|
||||
export const api: ExternalServiceApi = {
|
||||
handshake: handshakeHandler,
|
||||
pushToService: pushToServiceHandler,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { ExternalService, PushToServiceApiParams, ExecutorSubActionPushParams } from './types';
|
||||
|
||||
import { MapRecord } from '../case/common_types';
|
||||
import { MapRecord } from '../case/types';
|
||||
|
||||
const createMock = (): jest.Mocked<ExternalService> => {
|
||||
const service = {
|
||||
|
|
|
@ -5,11 +5,7 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import {
|
||||
CommentSchema,
|
||||
EntityInformation,
|
||||
IncidentConfigurationSchema,
|
||||
} from '../case/common_schema';
|
||||
import { CommentSchema, EntityInformation, IncidentConfigurationSchema } from '../case/schema';
|
||||
|
||||
export const ExternalIncidentServiceConfiguration = {
|
||||
apiUrl: schema.string(),
|
||||
|
|
|
@ -19,8 +19,8 @@ import {
|
|||
ExecutorSubActionGetFieldsByIssueTypeParamsSchema,
|
||||
} from './schema';
|
||||
import { ActionsConfigurationUtilities } from '../../actions_config';
|
||||
import { IncidentConfigurationSchema } from '../case/common_schema';
|
||||
import { Comment } from '../case/common_types';
|
||||
import { IncidentConfigurationSchema } from '../case/schema';
|
||||
import { Comment } from '../case/types';
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
|
||||
export type JiraPublicConfigurationType = TypeOf<typeof ExternalIncidentServiceConfigurationSchema>;
|
||||
|
|
|
@ -4,9 +4,12 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { api } from '../case/api';
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
import { api } from './api';
|
||||
import { externalServiceMock, mapping, apiParams } from './mocks';
|
||||
import { ExternalService } from '../case/types';
|
||||
import { ExternalService } from './types';
|
||||
|
||||
let mockedLogger: jest.Mocked<Logger>;
|
||||
|
||||
describe('api', () => {
|
||||
let externalService: jest.Mocked<ExternalService>;
|
||||
|
@ -23,7 +26,12 @@ describe('api', () => {
|
|||
describe('create incident', () => {
|
||||
test('it creates an incident', async () => {
|
||||
const params = { ...apiParams, externalId: null };
|
||||
const res = await api.pushToService({ externalService, mapping, params });
|
||||
const res = await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
id: '1',
|
||||
|
@ -45,7 +53,12 @@ describe('api', () => {
|
|||
|
||||
test('it creates an incident without comments', async () => {
|
||||
const params = { ...apiParams, externalId: null, comments: [] };
|
||||
const res = await api.pushToService({ externalService, mapping, params });
|
||||
const res = await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
id: '1',
|
||||
|
@ -57,7 +70,7 @@ describe('api', () => {
|
|||
|
||||
test('it calls createIncident correctly', async () => {
|
||||
const params = { ...apiParams, externalId: null };
|
||||
await api.pushToService({ externalService, mapping, params });
|
||||
await api.pushToService({ externalService, mapping, params, logger: mockedLogger });
|
||||
|
||||
expect(externalService.createIncident).toHaveBeenCalledWith({
|
||||
incident: {
|
||||
|
@ -71,7 +84,7 @@ describe('api', () => {
|
|||
|
||||
test('it calls createComment correctly', async () => {
|
||||
const params = { ...apiParams, externalId: null };
|
||||
await api.pushToService({ externalService, mapping, params });
|
||||
await api.pushToService({ externalService, mapping, params, logger: mockedLogger });
|
||||
expect(externalService.createComment).toHaveBeenCalledTimes(2);
|
||||
expect(externalService.createComment).toHaveBeenNthCalledWith(1, {
|
||||
incidentId: '1',
|
||||
|
@ -89,7 +102,6 @@ describe('api', () => {
|
|||
username: 'elastic',
|
||||
},
|
||||
},
|
||||
field: 'comments',
|
||||
});
|
||||
|
||||
expect(externalService.createComment).toHaveBeenNthCalledWith(2, {
|
||||
|
@ -108,14 +120,18 @@ describe('api', () => {
|
|||
username: 'elastic',
|
||||
},
|
||||
},
|
||||
field: 'comments',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('update incident', () => {
|
||||
test('it updates an incident', async () => {
|
||||
const res = await api.pushToService({ externalService, mapping, params: apiParams });
|
||||
const res = await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
id: '1',
|
||||
|
@ -137,7 +153,12 @@ describe('api', () => {
|
|||
|
||||
test('it updates an incident without comments', async () => {
|
||||
const params = { ...apiParams, comments: [] };
|
||||
const res = await api.pushToService({ externalService, mapping, params });
|
||||
const res = await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
id: '1',
|
||||
|
@ -149,7 +170,7 @@ describe('api', () => {
|
|||
|
||||
test('it calls updateIncident correctly', async () => {
|
||||
const params = { ...apiParams };
|
||||
await api.pushToService({ externalService, mapping, params });
|
||||
await api.pushToService({ externalService, mapping, params, logger: mockedLogger });
|
||||
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
|
@ -164,7 +185,7 @@ describe('api', () => {
|
|||
|
||||
test('it calls createComment correctly', async () => {
|
||||
const params = { ...apiParams };
|
||||
await api.pushToService({ externalService, mapping, params });
|
||||
await api.pushToService({ externalService, mapping, params, logger: mockedLogger });
|
||||
expect(externalService.createComment).toHaveBeenCalledTimes(2);
|
||||
expect(externalService.createComment).toHaveBeenNthCalledWith(1, {
|
||||
incidentId: '1',
|
||||
|
@ -182,7 +203,6 @@ describe('api', () => {
|
|||
username: 'elastic',
|
||||
},
|
||||
},
|
||||
field: 'comments',
|
||||
});
|
||||
|
||||
expect(externalService.createComment).toHaveBeenNthCalledWith(2, {
|
||||
|
@ -201,11 +221,52 @@ describe('api', () => {
|
|||
username: 'elastic',
|
||||
},
|
||||
},
|
||||
field: 'comments',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('incidentTypes', () => {
|
||||
test('it returns the incident types correctly', async () => {
|
||||
const res = await api.incidentTypes({
|
||||
externalService,
|
||||
params: {},
|
||||
});
|
||||
expect(res).toEqual([
|
||||
{
|
||||
id: 17,
|
||||
name: 'Communication error (fax; email)',
|
||||
},
|
||||
{
|
||||
id: 1001,
|
||||
name: 'Custom type',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('severity', () => {
|
||||
test('it returns the severity correctly', async () => {
|
||||
const res = await api.severity({
|
||||
externalService,
|
||||
params: { id: '10006' },
|
||||
});
|
||||
expect(res).toEqual([
|
||||
{
|
||||
id: 4,
|
||||
name: 'Low',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Medium',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'High',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapping variations', () => {
|
||||
test('overwrite & append', async () => {
|
||||
mapping.set('title', {
|
||||
|
@ -228,7 +289,12 @@ describe('api', () => {
|
|||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
await api.pushToService({ externalService, mapping, params: apiParams });
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
|
@ -260,7 +326,12 @@ describe('api', () => {
|
|||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
await api.pushToService({ externalService, mapping, params: apiParams });
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
|
@ -291,7 +362,12 @@ describe('api', () => {
|
|||
actionType: 'append',
|
||||
});
|
||||
|
||||
await api.pushToService({ externalService, mapping, params: apiParams });
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
|
@ -324,7 +400,12 @@ describe('api', () => {
|
|||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
await api.pushToService({ externalService, mapping, params: apiParams });
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {},
|
||||
|
@ -352,7 +433,12 @@ describe('api', () => {
|
|||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
await api.pushToService({ externalService, mapping, params: apiParams });
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
|
@ -382,7 +468,12 @@ describe('api', () => {
|
|||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
await api.pushToService({ externalService, mapping, params: apiParams });
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
|
@ -414,7 +505,12 @@ describe('api', () => {
|
|||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
await api.pushToService({ externalService, mapping, params: apiParams });
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
|
@ -445,7 +541,12 @@ describe('api', () => {
|
|||
actionType: 'append',
|
||||
});
|
||||
|
||||
await api.pushToService({ externalService, mapping, params: apiParams });
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
|
@ -478,7 +579,12 @@ describe('api', () => {
|
|||
actionType: 'append',
|
||||
});
|
||||
|
||||
await api.pushToService({ externalService, mapping, params: apiParams });
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
|
@ -509,7 +615,12 @@ describe('api', () => {
|
|||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
await api.pushToService({ externalService, mapping, params: apiParams });
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.createComment).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,4 +4,129 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { api } from '../case/api';
|
||||
import {
|
||||
ExternalServiceParams,
|
||||
PushToServiceApiHandlerArgs,
|
||||
HandshakeApiHandlerArgs,
|
||||
GetIncidentApiHandlerArgs,
|
||||
ExternalServiceApi,
|
||||
Incident,
|
||||
GetIncidentTypesHandlerArgs,
|
||||
GetSeverityHandlerArgs,
|
||||
PushToServiceApiParams,
|
||||
PushToServiceResponse,
|
||||
} from './types';
|
||||
|
||||
// TODO: to remove, need to support Case
|
||||
import { transformFields, prepareFieldsForTransformation, transformComments } from '../case/utils';
|
||||
|
||||
const handshakeHandler = async ({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
}: HandshakeApiHandlerArgs) => {};
|
||||
|
||||
const getIncidentHandler = async ({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
}: GetIncidentApiHandlerArgs) => {};
|
||||
|
||||
const getIncidentTypesHandler = async ({ externalService }: GetIncidentTypesHandlerArgs) => {
|
||||
const res = await externalService.getIncidentTypes();
|
||||
return res;
|
||||
};
|
||||
|
||||
const getSeverityHandler = async ({ externalService }: GetSeverityHandlerArgs) => {
|
||||
const res = await externalService.getSeverity();
|
||||
return res;
|
||||
};
|
||||
|
||||
const pushToServiceHandler = async ({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
logger,
|
||||
}: PushToServiceApiHandlerArgs): Promise<PushToServiceResponse> => {
|
||||
const { externalId, comments } = params;
|
||||
const updateIncident = externalId ? true : false;
|
||||
const defaultPipes = updateIncident ? ['informationUpdated'] : ['informationCreated'];
|
||||
let currentIncident: ExternalServiceParams | undefined;
|
||||
let res: PushToServiceResponse;
|
||||
|
||||
if (externalId) {
|
||||
try {
|
||||
currentIncident = await externalService.getIncident(externalId);
|
||||
} catch (ex) {
|
||||
logger.debug(
|
||||
`Retrieving Incident by id ${externalId} from IBM Resilient was failed with exception: ${ex}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let incident: Incident;
|
||||
// TODO: should be removed later but currently keep it for the Case implementation support
|
||||
if (mapping) {
|
||||
const fields = prepareFieldsForTransformation({
|
||||
externalCase: params.externalObject,
|
||||
mapping,
|
||||
defaultPipes,
|
||||
});
|
||||
|
||||
incident = transformFields<PushToServiceApiParams, ExternalServiceParams, Incident>({
|
||||
params,
|
||||
fields,
|
||||
currentIncident,
|
||||
});
|
||||
} else {
|
||||
const { title, description, incidentTypes, severityCode } = params;
|
||||
incident = { name: title, description, incidentTypes, severityCode };
|
||||
}
|
||||
|
||||
if (externalId != null) {
|
||||
res = await externalService.updateIncident({
|
||||
incidentId: externalId,
|
||||
incident,
|
||||
});
|
||||
} else {
|
||||
res = await externalService.createIncident({
|
||||
incident: {
|
||||
...incident,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (comments && Array.isArray(comments) && comments.length > 0) {
|
||||
if (mapping && mapping.get('comments')?.actionType === 'nothing') {
|
||||
return res;
|
||||
}
|
||||
const commentsTransformed = mapping
|
||||
? transformComments(comments, ['informationAdded'])
|
||||
: comments;
|
||||
|
||||
res.comments = [];
|
||||
for (const currentComment of commentsTransformed) {
|
||||
const comment = await externalService.createComment({
|
||||
incidentId: res.id,
|
||||
comment: currentComment,
|
||||
});
|
||||
res.comments = [
|
||||
...(res.comments ?? []),
|
||||
{
|
||||
commentId: comment.commentId,
|
||||
pushedDate: comment.pushedDate,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const api: ExternalServiceApi = {
|
||||
handshake: handshakeHandler,
|
||||
pushToService: pushToServiceHandler,
|
||||
getIncident: getIncidentHandler,
|
||||
incidentTypes: getIncidentTypesHandler,
|
||||
severity: getSeverityHandler,
|
||||
};
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ExternalServiceConfiguration } from '../case/types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const config: ExternalServiceConfiguration = {
|
||||
id: '.resilient',
|
||||
name: i18n.NAME,
|
||||
minimumLicenseRequired: 'platinum',
|
||||
};
|
|
@ -4,33 +4,139 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
import { createConnector } from '../case/utils';
|
||||
import { curry } from 'lodash';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
import { api } from './api';
|
||||
import { config } from './config';
|
||||
import { validate } from './validators';
|
||||
import { createExternalService } from './service';
|
||||
import { ResilientSecretConfiguration, ResilientPublicConfiguration } from './schema';
|
||||
import {
|
||||
ExternalIncidentServiceConfiguration,
|
||||
ExternalIncidentServiceSecretConfiguration,
|
||||
ExecutorParamsSchema,
|
||||
} from './schema';
|
||||
import { ActionsConfigurationUtilities } from '../../actions_config';
|
||||
import { ActionType } from '../../types';
|
||||
import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../../types';
|
||||
import { createExternalService } from './service';
|
||||
import { api } from './api';
|
||||
import {
|
||||
ExecutorParams,
|
||||
ExecutorSubActionPushParams,
|
||||
ResilientPublicConfigurationType,
|
||||
ResilientSecretConfigurationType,
|
||||
ResilientExecutorResultData,
|
||||
ExecutorSubActionGetIncidentTypesParams,
|
||||
ExecutorSubActionGetSeverityParams,
|
||||
} from './types';
|
||||
import * as i18n from './translations';
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
|
||||
export function getActionType({
|
||||
logger,
|
||||
configurationUtilities,
|
||||
}: {
|
||||
// TODO: to remove, need to support Case
|
||||
import { buildMap, mapParams } from '../case/utils';
|
||||
|
||||
interface GetActionTypeParams {
|
||||
logger: Logger;
|
||||
configurationUtilities: ActionsConfigurationUtilities;
|
||||
}): ActionType {
|
||||
return createConnector({
|
||||
api,
|
||||
config,
|
||||
validate,
|
||||
createExternalService,
|
||||
validationSchema: {
|
||||
config: ResilientPublicConfiguration,
|
||||
secrets: ResilientSecretConfiguration,
|
||||
}
|
||||
|
||||
const supportedSubActions: string[] = ['pushToService', 'incidentTypes', 'severity'];
|
||||
|
||||
// action type definition
|
||||
export function getActionType(
|
||||
params: GetActionTypeParams
|
||||
): ActionType<
|
||||
ResilientPublicConfigurationType,
|
||||
ResilientSecretConfigurationType,
|
||||
ExecutorParams,
|
||||
ResilientExecutorResultData | {}
|
||||
> {
|
||||
const { logger, configurationUtilities } = params;
|
||||
return {
|
||||
id: '.resilient',
|
||||
minimumLicenseRequired: 'platinum',
|
||||
name: i18n.NAME,
|
||||
validate: {
|
||||
config: schema.object(ExternalIncidentServiceConfiguration, {
|
||||
validate: curry(validate.config)(configurationUtilities),
|
||||
}),
|
||||
secrets: schema.object(ExternalIncidentServiceSecretConfiguration, {
|
||||
validate: curry(validate.secrets)(configurationUtilities),
|
||||
}),
|
||||
params: ExecutorParamsSchema,
|
||||
},
|
||||
executor: curry(executor)({ logger }),
|
||||
};
|
||||
}
|
||||
|
||||
// action executor
|
||||
async function executor(
|
||||
{ logger }: { logger: Logger },
|
||||
execOptions: ActionTypeExecutorOptions<
|
||||
ResilientPublicConfigurationType,
|
||||
ResilientSecretConfigurationType,
|
||||
ExecutorParams
|
||||
>
|
||||
): Promise<ActionTypeExecutorResult<ResilientExecutorResultData | {}>> {
|
||||
const { actionId, config, params, secrets } = execOptions;
|
||||
const { subAction, subActionParams } = params as ExecutorParams;
|
||||
let data: ResilientExecutorResultData | null = null;
|
||||
|
||||
const externalService = createExternalService(
|
||||
{
|
||||
config,
|
||||
secrets,
|
||||
},
|
||||
logger,
|
||||
})({ configurationUtilities });
|
||||
execOptions.proxySettings
|
||||
);
|
||||
|
||||
if (!api[subAction]) {
|
||||
const errorMessage = `[Action][ExternalService] Unsupported subAction type ${subAction}.`;
|
||||
logger.error(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
if (!supportedSubActions.includes(subAction)) {
|
||||
const errorMessage = `[Action][ExternalService] subAction ${subAction} not implemented.`;
|
||||
logger.error(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
if (subAction === 'pushToService') {
|
||||
const pushToServiceParams = subActionParams as ExecutorSubActionPushParams;
|
||||
|
||||
const { comments, externalId, ...restParams } = pushToServiceParams;
|
||||
const mapping = config.incidentConfiguration
|
||||
? buildMap(config.incidentConfiguration.mapping)
|
||||
: null;
|
||||
const externalObject =
|
||||
config.incidentConfiguration && mapping
|
||||
? mapParams<ExecutorSubActionPushParams>(restParams as ExecutorSubActionPushParams, mapping)
|
||||
: {};
|
||||
|
||||
data = await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: { ...pushToServiceParams, externalObject },
|
||||
logger,
|
||||
});
|
||||
|
||||
logger.debug(`response push to service for incident id: ${data.id}`);
|
||||
}
|
||||
|
||||
if (subAction === 'incidentTypes') {
|
||||
const incidentTypesParams = subActionParams as ExecutorSubActionGetIncidentTypesParams;
|
||||
data = await api.incidentTypes({
|
||||
externalService,
|
||||
params: incidentTypesParams,
|
||||
});
|
||||
}
|
||||
|
||||
if (subAction === 'severity') {
|
||||
const severityParams = subActionParams as ExecutorSubActionGetSeverityParams;
|
||||
data = await api.severity({
|
||||
externalService,
|
||||
params: severityParams,
|
||||
});
|
||||
}
|
||||
|
||||
return { status: 'ok', data: data ?? {}, actionId };
|
||||
}
|
||||
|
|
|
@ -4,12 +4,9 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
ExternalService,
|
||||
PushToServiceApiParams,
|
||||
ExecutorSubActionPushParams,
|
||||
MapRecord,
|
||||
} from '../case/types';
|
||||
import { ExternalService, PushToServiceApiParams, ExecutorSubActionPushParams } from './types';
|
||||
|
||||
import { MapRecord } from '../case/types';
|
||||
|
||||
const createMock = (): jest.Mocked<ExternalService> => {
|
||||
const service = {
|
||||
|
@ -40,6 +37,25 @@ const createMock = (): jest.Mocked<ExternalService> => {
|
|||
})
|
||||
),
|
||||
createComment: jest.fn(),
|
||||
findIncidents: jest.fn(),
|
||||
getIncidentTypes: jest.fn().mockImplementation(() => [
|
||||
{ id: 17, name: 'Communication error (fax; email)' },
|
||||
{ id: 1001, name: 'Custom type' },
|
||||
]),
|
||||
getSeverity: jest.fn().mockImplementation(() => [
|
||||
{
|
||||
id: 4,
|
||||
name: 'Low',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Medium',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'High',
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
service.createComment.mockImplementationOnce(() =>
|
||||
|
@ -96,6 +112,8 @@ const executorParams: ExecutorSubActionPushParams = {
|
|||
updatedBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
title: 'Incident title',
|
||||
description: 'Incident description',
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
comments: [
|
||||
{
|
||||
commentId: 'case-comment-1',
|
||||
|
@ -118,7 +136,58 @@ const executorParams: ExecutorSubActionPushParams = {
|
|||
|
||||
const apiParams: PushToServiceApiParams = {
|
||||
...executorParams,
|
||||
externalCase: { name: 'Incident title', description: 'Incident description' },
|
||||
externalObject: { name: 'Incident title', description: 'Incident description' },
|
||||
};
|
||||
|
||||
export { externalServiceMock, mapping, executorParams, apiParams };
|
||||
const incidentTypes = [
|
||||
{
|
||||
value: 17,
|
||||
label: 'Communication error (fax; email)',
|
||||
enabled: true,
|
||||
properties: null,
|
||||
uuid: '4a8d22f7-d89e-4403-85c7-2bafe3b7f2ae',
|
||||
hidden: false,
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
value: 1001,
|
||||
label: 'Custom type',
|
||||
enabled: true,
|
||||
properties: null,
|
||||
uuid: '3b51c8c2-9758-48f8-b013-bd141f1d2ec9',
|
||||
hidden: false,
|
||||
default: false,
|
||||
},
|
||||
];
|
||||
|
||||
const severity = [
|
||||
{
|
||||
value: 4,
|
||||
label: 'Low',
|
||||
enabled: true,
|
||||
properties: null,
|
||||
uuid: '97cae239-963d-4e36-be34-07e47ef2cc86',
|
||||
hidden: false,
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
value: 5,
|
||||
label: 'Medium',
|
||||
enabled: true,
|
||||
properties: null,
|
||||
uuid: 'c2c354c9-6d1e-4a48-82e5-bd5dc5068339',
|
||||
hidden: false,
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
value: 6,
|
||||
label: 'High',
|
||||
enabled: true,
|
||||
properties: null,
|
||||
uuid: '93e5c99c-563b-48b9-80a3-9572307622d8',
|
||||
hidden: false,
|
||||
default: false,
|
||||
},
|
||||
];
|
||||
|
||||
export { externalServiceMock, mapping, executorParams, apiParams, incidentTypes, severity };
|
||||
|
|
|
@ -5,18 +5,77 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { ExternalIncidentServiceConfiguration } from '../case/schema';
|
||||
import { CommentSchema, EntityInformation, IncidentConfigurationSchema } from '../case/schema';
|
||||
|
||||
export const ResilientPublicConfiguration = {
|
||||
export const ExternalIncidentServiceConfiguration = {
|
||||
apiUrl: schema.string(),
|
||||
orgId: schema.string(),
|
||||
...ExternalIncidentServiceConfiguration,
|
||||
// TODO: to remove - set it optional for the current stage to support Case implementation
|
||||
incidentConfiguration: schema.nullable(IncidentConfigurationSchema),
|
||||
isCaseOwned: schema.nullable(schema.boolean()),
|
||||
};
|
||||
|
||||
export const ResilientPublicConfigurationSchema = schema.object(ResilientPublicConfiguration);
|
||||
export const ExternalIncidentServiceConfigurationSchema = schema.object(
|
||||
ExternalIncidentServiceConfiguration
|
||||
);
|
||||
|
||||
export const ResilientSecretConfiguration = {
|
||||
export const ExternalIncidentServiceSecretConfiguration = {
|
||||
apiKeyId: schema.string(),
|
||||
apiKeySecret: schema.string(),
|
||||
};
|
||||
|
||||
export const ResilientSecretConfigurationSchema = schema.object(ResilientSecretConfiguration);
|
||||
export const ExternalIncidentServiceSecretConfigurationSchema = schema.object(
|
||||
ExternalIncidentServiceSecretConfiguration
|
||||
);
|
||||
|
||||
export const ExecutorSubActionSchema = schema.oneOf([
|
||||
schema.literal('getIncident'),
|
||||
schema.literal('pushToService'),
|
||||
schema.literal('handshake'),
|
||||
schema.literal('incidentTypes'),
|
||||
schema.literal('severity'),
|
||||
]);
|
||||
|
||||
export const ExecutorSubActionPushParamsSchema = schema.object({
|
||||
savedObjectId: schema.string(),
|
||||
title: schema.string(),
|
||||
description: schema.nullable(schema.string()),
|
||||
externalId: schema.nullable(schema.string()),
|
||||
incidentTypes: schema.nullable(schema.arrayOf(schema.number())),
|
||||
severityCode: schema.nullable(schema.number()),
|
||||
// TODO: remove later - need for support Case push multiple comments
|
||||
comments: schema.nullable(schema.arrayOf(CommentSchema)),
|
||||
...EntityInformation,
|
||||
});
|
||||
|
||||
export const ExecutorSubActionGetIncidentParamsSchema = schema.object({
|
||||
externalId: schema.string(),
|
||||
});
|
||||
|
||||
// Reserved for future implementation
|
||||
export const ExecutorSubActionHandshakeParamsSchema = schema.object({});
|
||||
export const ExecutorSubActionGetIncidentTypesParamsSchema = schema.object({});
|
||||
export const ExecutorSubActionGetSeverityParamsSchema = schema.object({});
|
||||
|
||||
export const ExecutorParamsSchema = schema.oneOf([
|
||||
schema.object({
|
||||
subAction: schema.literal('getIncident'),
|
||||
subActionParams: ExecutorSubActionGetIncidentParamsSchema,
|
||||
}),
|
||||
schema.object({
|
||||
subAction: schema.literal('handshake'),
|
||||
subActionParams: ExecutorSubActionHandshakeParamsSchema,
|
||||
}),
|
||||
schema.object({
|
||||
subAction: schema.literal('pushToService'),
|
||||
subActionParams: ExecutorSubActionPushParamsSchema,
|
||||
}),
|
||||
schema.object({
|
||||
subAction: schema.literal('incidentTypes'),
|
||||
subActionParams: ExecutorSubActionGetIncidentTypesParamsSchema,
|
||||
}),
|
||||
schema.object({
|
||||
subAction: schema.literal('severity'),
|
||||
subActionParams: ExecutorSubActionGetSeverityParamsSchema,
|
||||
}),
|
||||
]);
|
||||
|
|
|
@ -8,9 +8,11 @@ import axios from 'axios';
|
|||
|
||||
import { createExternalService, getValueTextContent, formatUpdateRequest } from './service';
|
||||
import * as utils from '../lib/axios_utils';
|
||||
import { ExternalService } from '../case/types';
|
||||
import { ExternalService } from './types';
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
|
||||
import { incidentTypes, severity } from './mocks';
|
||||
|
||||
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
|
||||
jest.mock('axios');
|
||||
|
@ -41,6 +43,8 @@ const mockIncidentUpdate = (withUpdateError = false) => {
|
|||
format: 'html',
|
||||
content: 'description',
|
||||
},
|
||||
incident_type_ids: [1001, 16, 12],
|
||||
severity_code: 6,
|
||||
},
|
||||
}));
|
||||
|
||||
|
@ -246,7 +250,12 @@ describe('IBM Resilient service', () => {
|
|||
}));
|
||||
|
||||
const res = await service.createIncident({
|
||||
incident: { name: 'title', description: 'desc' },
|
||||
incident: {
|
||||
name: 'title',
|
||||
description: 'desc',
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
|
@ -269,12 +278,18 @@ describe('IBM Resilient service', () => {
|
|||
}));
|
||||
|
||||
await service.createIncident({
|
||||
incident: { name: 'title', description: 'desc' },
|
||||
incident: {
|
||||
name: 'title',
|
||||
description: 'desc',
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
},
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
axios,
|
||||
url: 'https://resilient.elastic.co/rest/orgs/201/incidents',
|
||||
url:
|
||||
'https://resilient.elastic.co/rest/orgs/201/incidents?text_content_output_format=objects_convert',
|
||||
logger,
|
||||
method: 'post',
|
||||
data: {
|
||||
|
@ -284,6 +299,8 @@ describe('IBM Resilient service', () => {
|
|||
content: 'desc',
|
||||
},
|
||||
discovered_date: TIMESTAMP,
|
||||
incident_type_ids: [{ id: 1001 }],
|
||||
severity_code: { id: 6 },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -295,7 +312,12 @@ describe('IBM Resilient service', () => {
|
|||
|
||||
expect(
|
||||
service.createIncident({
|
||||
incident: { name: 'title', description: 'desc' },
|
||||
incident: {
|
||||
name: 'title',
|
||||
description: 'desc',
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
},
|
||||
})
|
||||
).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to create incident. Error: An error has occurred'
|
||||
|
@ -308,7 +330,12 @@ describe('IBM Resilient service', () => {
|
|||
mockIncidentUpdate();
|
||||
const res = await service.updateIncident({
|
||||
incidentId: '1',
|
||||
incident: { name: 'title_updated', description: 'desc_updated' },
|
||||
incident: {
|
||||
name: 'title',
|
||||
description: 'desc',
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
|
@ -324,7 +351,12 @@ describe('IBM Resilient service', () => {
|
|||
|
||||
await service.updateIncident({
|
||||
incidentId: '1',
|
||||
incident: { name: 'title_updated', description: 'desc_updated' },
|
||||
incident: {
|
||||
name: 'title_updated',
|
||||
description: 'desc_updated',
|
||||
incidentTypes: [1001],
|
||||
severityCode: 5,
|
||||
},
|
||||
});
|
||||
|
||||
// Incident update makes three calls to the API.
|
||||
|
@ -356,6 +388,28 @@ describe('IBM Resilient service', () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
name: 'incident_type_ids',
|
||||
},
|
||||
old_value: {
|
||||
ids: [1001, 16, 12],
|
||||
},
|
||||
new_value: {
|
||||
ids: [1001],
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
name: 'severity_code',
|
||||
},
|
||||
old_value: {
|
||||
id: 6,
|
||||
},
|
||||
new_value: {
|
||||
id: 5,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
@ -367,7 +421,12 @@ describe('IBM Resilient service', () => {
|
|||
expect(
|
||||
service.updateIncident({
|
||||
incidentId: '1',
|
||||
incident: { name: 'title', description: 'desc' },
|
||||
incident: {
|
||||
name: 'title',
|
||||
description: 'desc',
|
||||
incidentTypes: [1001],
|
||||
severityCode: 5,
|
||||
},
|
||||
})
|
||||
).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to update incident with id 1. Error: An error has occurred'
|
||||
|
@ -386,8 +445,14 @@ describe('IBM Resilient service', () => {
|
|||
|
||||
const res = await service.createComment({
|
||||
incidentId: '1',
|
||||
comment: { comment: 'comment', commentId: 'comment-1' },
|
||||
field: 'comments',
|
||||
comment: {
|
||||
comment: 'comment',
|
||||
commentId: 'comment-1',
|
||||
createdBy: null,
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
|
@ -407,8 +472,14 @@ describe('IBM Resilient service', () => {
|
|||
|
||||
await service.createComment({
|
||||
incidentId: '1',
|
||||
comment: { comment: 'comment', commentId: 'comment-1' },
|
||||
field: 'my_field',
|
||||
comment: {
|
||||
comment: 'comment',
|
||||
commentId: 'comment-1',
|
||||
createdBy: null,
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
},
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
|
@ -434,12 +505,82 @@ describe('IBM Resilient service', () => {
|
|||
expect(
|
||||
service.createComment({
|
||||
incidentId: '1',
|
||||
comment: { comment: 'comment', commentId: 'comment-1' },
|
||||
field: 'comments',
|
||||
comment: {
|
||||
comment: 'comment',
|
||||
commentId: 'comment-1',
|
||||
createdBy: null,
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
},
|
||||
})
|
||||
).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to create comment at incident with id 1. Error: An error has occurred'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIncidentTypes', () => {
|
||||
test('it creates the incident correctly', async () => {
|
||||
requestMock.mockImplementation(() => ({
|
||||
data: {
|
||||
values: incidentTypes,
|
||||
},
|
||||
}));
|
||||
|
||||
const res = await service.getIncidentTypes();
|
||||
|
||||
expect(res).toEqual([
|
||||
{ id: 17, name: 'Communication error (fax; email)' },
|
||||
{ id: 1001, name: 'Custom type' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('it should throw an error', async () => {
|
||||
requestMock.mockImplementation(() => {
|
||||
throw new Error('An error has occurred');
|
||||
});
|
||||
|
||||
expect(service.getIncidentTypes()).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to get incident types. Error: An error has occurred.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSeverity', () => {
|
||||
test('it creates the incident correctly', async () => {
|
||||
requestMock.mockImplementation(() => ({
|
||||
data: {
|
||||
values: severity,
|
||||
},
|
||||
}));
|
||||
|
||||
const res = await service.getSeverity();
|
||||
|
||||
expect(res).toEqual([
|
||||
{
|
||||
id: 4,
|
||||
name: 'Low',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Medium',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'High',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('it should throw an error', async () => {
|
||||
requestMock.mockImplementation(() => {
|
||||
throw new Error('An error has occurred');
|
||||
});
|
||||
|
||||
expect(service.getIncidentTypes()).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to get incident types. Error: An error has occurred.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,44 +5,56 @@
|
|||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import { omitBy, isNil } from 'lodash/fp';
|
||||
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types';
|
||||
import {
|
||||
ExternalServiceCredentials,
|
||||
ExternalService,
|
||||
ExternalServiceParams,
|
||||
CreateCommentParams,
|
||||
UpdateIncidentParams,
|
||||
CreateIncidentParams,
|
||||
CreateIncidentData,
|
||||
ResilientPublicConfigurationType,
|
||||
ResilientSecretConfigurationType,
|
||||
CreateIncidentRequest,
|
||||
UpdateIncidentRequest,
|
||||
CreateCommentRequest,
|
||||
UpdateFieldText,
|
||||
UpdateFieldTextArea,
|
||||
GetValueTextContentResponse,
|
||||
} from './types';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { getErrorMessage, request } from '../lib/axios_utils';
|
||||
import { ProxySettings } from '../../types';
|
||||
|
||||
const BASE_URL = `rest`;
|
||||
const INCIDENT_URL = `incidents`;
|
||||
const COMMENT_URL = `comments`;
|
||||
|
||||
const VIEW_INCIDENT_URL = `#incidents`;
|
||||
|
||||
export const getValueTextContent = (
|
||||
field: string,
|
||||
value: string
|
||||
): UpdateFieldText | UpdateFieldTextArea => {
|
||||
value: string | number | number[]
|
||||
): GetValueTextContentResponse => {
|
||||
if (field === 'description') {
|
||||
return {
|
||||
textarea: {
|
||||
format: 'html',
|
||||
content: value,
|
||||
content: value as string,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (field === 'incidentTypes') {
|
||||
return {
|
||||
ids: value as number[],
|
||||
};
|
||||
}
|
||||
|
||||
if (field === 'severityCode') {
|
||||
return {
|
||||
id: value as number,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
text: value,
|
||||
text: value as string,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -51,11 +63,30 @@ export const formatUpdateRequest = ({
|
|||
newIncident,
|
||||
}: ExternalServiceParams): UpdateIncidentRequest => {
|
||||
return {
|
||||
changes: Object.keys(newIncident).map((key) => ({
|
||||
field: { name: key },
|
||||
old_value: getValueTextContent(key, oldIncident[key]),
|
||||
new_value: getValueTextContent(key, newIncident[key]),
|
||||
})),
|
||||
changes: Object.keys(newIncident as Record<string, unknown>).map((key) => {
|
||||
let name = key;
|
||||
|
||||
if (key === 'incidentTypes') {
|
||||
name = 'incident_type_ids';
|
||||
}
|
||||
|
||||
if (key === 'severityCode') {
|
||||
name = 'severity_code';
|
||||
}
|
||||
|
||||
return {
|
||||
field: { name },
|
||||
// TODO: Fix ugly casting
|
||||
old_value: getValueTextContent(
|
||||
key,
|
||||
(oldIncident as Record<string, unknown>)[name] as string
|
||||
),
|
||||
new_value: getValueTextContent(
|
||||
key,
|
||||
(newIncident as Record<string, unknown>)[key] as string
|
||||
),
|
||||
};
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -72,8 +103,12 @@ export const createExternalService = (
|
|||
}
|
||||
|
||||
const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url;
|
||||
const incidentUrl = `${urlWithoutTrailingSlash}/${BASE_URL}/orgs/${orgId}/${INCIDENT_URL}`;
|
||||
const commentUrl = `${incidentUrl}/{inc_id}/${COMMENT_URL}`;
|
||||
const orgUrl = `${urlWithoutTrailingSlash}/rest/orgs/${orgId}`;
|
||||
const incidentUrl = `${orgUrl}/incidents`;
|
||||
const commentUrl = `${incidentUrl}/{inc_id}/comments`;
|
||||
const incidentFieldsUrl = `${orgUrl}/types/incident/fields`;
|
||||
const incidentTypesUrl = `${incidentFieldsUrl}/incident_type_ids`;
|
||||
const severityUrl = `${incidentFieldsUrl}/severity_code`;
|
||||
const axiosInstance = axios.create({
|
||||
auth: { username: apiKeyId, password: apiKeySecret },
|
||||
});
|
||||
|
@ -101,26 +136,48 @@ export const createExternalService = (
|
|||
return { ...res.data, description: res.data.description?.content ?? '' };
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
getErrorMessage(i18n.NAME, `Unable to get incident with id ${id}. Error: ${error.message}`)
|
||||
getErrorMessage(i18n.NAME, `Unable to get incident with id ${id}. Error: ${error.message}.`)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const createIncident = async ({ incident }: ExternalServiceParams) => {
|
||||
const createIncident = async ({ incident }: CreateIncidentParams) => {
|
||||
let data: CreateIncidentData = {
|
||||
name: incident.name,
|
||||
discovered_date: Date.now(),
|
||||
};
|
||||
|
||||
if (incident.description) {
|
||||
data = {
|
||||
...data,
|
||||
description: {
|
||||
format: 'html',
|
||||
content: incident.description ?? '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (incident.incidentTypes) {
|
||||
data = {
|
||||
...data,
|
||||
incident_type_ids: incident.incidentTypes.map((id) => ({ id })),
|
||||
};
|
||||
}
|
||||
|
||||
if (incident.severityCode) {
|
||||
data = {
|
||||
...data,
|
||||
severity_code: { id: incident.severityCode },
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await request<CreateIncidentRequest>({
|
||||
const res = await request({
|
||||
axios: axiosInstance,
|
||||
url: `${incidentUrl}`,
|
||||
url: `${incidentUrl}?text_content_output_format=objects_convert`,
|
||||
method: 'post',
|
||||
logger,
|
||||
data: {
|
||||
...incident,
|
||||
description: {
|
||||
format: 'html',
|
||||
content: incident.description ?? '',
|
||||
},
|
||||
discovered_date: Date.now(),
|
||||
},
|
||||
data,
|
||||
proxySettings,
|
||||
});
|
||||
|
||||
|
@ -132,17 +189,20 @@ export const createExternalService = (
|
|||
};
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
getErrorMessage(i18n.NAME, `Unable to create incident. Error: ${error.message}`)
|
||||
getErrorMessage(i18n.NAME, `Unable to create incident. Error: ${error.message}.`)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const updateIncident = async ({ incidentId, incident }: ExternalServiceParams) => {
|
||||
const updateIncident = async ({ incidentId, incident }: UpdateIncidentParams) => {
|
||||
try {
|
||||
const latestIncident = await getIncident(incidentId);
|
||||
|
||||
const data = formatUpdateRequest({ oldIncident: latestIncident, newIncident: incident });
|
||||
const res = await request<UpdateIncidentRequest>({
|
||||
// Remove null or undefined values. Allowing null values sets the field in IBM Resilient to empty.
|
||||
const newIncident = omitBy(isNil, incident);
|
||||
const data = formatUpdateRequest({ oldIncident: latestIncident, newIncident });
|
||||
|
||||
const res = await request({
|
||||
axios: axiosInstance,
|
||||
method: 'patch',
|
||||
url: `${incidentUrl}/${incidentId}`,
|
||||
|
@ -173,9 +233,9 @@ export const createExternalService = (
|
|||
}
|
||||
};
|
||||
|
||||
const createComment = async ({ incidentId, comment, field }: ExternalServiceParams) => {
|
||||
const createComment = async ({ incidentId, comment }: CreateCommentParams) => {
|
||||
try {
|
||||
const res = await request<CreateCommentRequest>({
|
||||
const res = await request({
|
||||
axios: axiosInstance,
|
||||
method: 'post',
|
||||
url: getCommentsURL(incidentId),
|
||||
|
@ -193,16 +253,62 @@ export const createExternalService = (
|
|||
throw new Error(
|
||||
getErrorMessage(
|
||||
i18n.NAME,
|
||||
`Unable to create comment at incident with id ${incidentId}. Error: ${error.message}`
|
||||
`Unable to create comment at incident with id ${incidentId}. Error: ${error.message}.`
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getIncidentTypes = async () => {
|
||||
try {
|
||||
const res = await request({
|
||||
axios: axiosInstance,
|
||||
method: 'get',
|
||||
url: incidentTypesUrl,
|
||||
logger,
|
||||
proxySettings,
|
||||
});
|
||||
|
||||
const incidentTypes = res.data?.values ?? [];
|
||||
return incidentTypes.map((type: { value: string; label: string }) => ({
|
||||
id: type.value,
|
||||
name: type.label,
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
getErrorMessage(i18n.NAME, `Unable to get incident types. Error: ${error.message}.`)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getSeverity = async () => {
|
||||
try {
|
||||
const res = await request({
|
||||
axios: axiosInstance,
|
||||
method: 'get',
|
||||
url: severityUrl,
|
||||
logger,
|
||||
proxySettings,
|
||||
});
|
||||
|
||||
const incidentTypes = res.data?.values ?? [];
|
||||
return incidentTypes.map((type: { value: string; label: string }) => ({
|
||||
id: type.value,
|
||||
name: type.label,
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
getErrorMessage(i18n.NAME, `Unable to get severity. Error: ${error.message}.`)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getIncident,
|
||||
createIncident,
|
||||
updateIncident,
|
||||
createComment,
|
||||
getIncidentTypes,
|
||||
getSeverity,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -9,3 +9,19 @@ import { i18n } from '@kbn/i18n';
|
|||
export const NAME = i18n.translate('xpack.actions.builtin.case.resilientTitle', {
|
||||
defaultMessage: 'IBM Resilient',
|
||||
});
|
||||
|
||||
export const ALLOWED_HOSTS_ERROR = (message: string) =>
|
||||
i18n.translate('xpack.actions.builtin.configuration.apiAllowedHostsError', {
|
||||
defaultMessage: 'error configuring connector action: {message}',
|
||||
values: {
|
||||
message,
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: remove when Case mappings will be removed
|
||||
export const MAPPING_EMPTY = i18n.translate(
|
||||
'xpack.actions.builtin.servicenow.configuration.emptyMapping',
|
||||
{
|
||||
defaultMessage: '[incidentConfiguration.mapping]: expected non-empty but got empty',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -4,29 +4,175 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import { ResilientPublicConfigurationSchema, ResilientSecretConfigurationSchema } from './schema';
|
||||
import {
|
||||
ExternalIncidentServiceConfigurationSchema,
|
||||
ExternalIncidentServiceSecretConfigurationSchema,
|
||||
ExecutorParamsSchema,
|
||||
ExecutorSubActionPushParamsSchema,
|
||||
ExecutorSubActionGetIncidentParamsSchema,
|
||||
ExecutorSubActionHandshakeParamsSchema,
|
||||
ExecutorSubActionGetIncidentTypesParamsSchema,
|
||||
ExecutorSubActionGetSeverityParamsSchema,
|
||||
} from './schema';
|
||||
|
||||
export type ResilientPublicConfigurationType = TypeOf<typeof ResilientPublicConfigurationSchema>;
|
||||
export type ResilientSecretConfigurationType = TypeOf<typeof ResilientSecretConfigurationSchema>;
|
||||
import { ActionsConfigurationUtilities } from '../../actions_config';
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
|
||||
interface CreateIncidentBasicRequestArgs {
|
||||
import { IncidentConfigurationSchema } from '../case/schema';
|
||||
import { Comment } from '../case/types';
|
||||
|
||||
export type ResilientPublicConfigurationType = TypeOf<
|
||||
typeof ExternalIncidentServiceConfigurationSchema
|
||||
>;
|
||||
export type ResilientSecretConfigurationType = TypeOf<
|
||||
typeof ExternalIncidentServiceSecretConfigurationSchema
|
||||
>;
|
||||
|
||||
export type ExecutorParams = TypeOf<typeof ExecutorParamsSchema>;
|
||||
export type ExecutorSubActionPushParams = TypeOf<typeof ExecutorSubActionPushParamsSchema>;
|
||||
|
||||
export type IncidentConfiguration = TypeOf<typeof IncidentConfigurationSchema>;
|
||||
|
||||
export interface ExternalServiceCredentials {
|
||||
config: Record<string, unknown>;
|
||||
secrets: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ExternalServiceValidation {
|
||||
config: (configurationUtilities: ActionsConfigurationUtilities, configObject: any) => void;
|
||||
secrets: (configurationUtilities: ActionsConfigurationUtilities, secrets: any) => void;
|
||||
}
|
||||
|
||||
export interface ExternalServiceIncidentResponse {
|
||||
id: string;
|
||||
title: string;
|
||||
url: string;
|
||||
pushedDate: string;
|
||||
}
|
||||
|
||||
export interface ExternalServiceCommentResponse {
|
||||
commentId: string;
|
||||
pushedDate: string;
|
||||
externalCommentId?: string;
|
||||
}
|
||||
|
||||
export type ExternalServiceParams = Record<string, unknown>;
|
||||
|
||||
export type Incident = Pick<
|
||||
ExecutorSubActionPushParams,
|
||||
'description' | 'incidentTypes' | 'severityCode'
|
||||
> & {
|
||||
name: string;
|
||||
description: string;
|
||||
discovered_date: number;
|
||||
};
|
||||
|
||||
export interface CreateIncidentParams {
|
||||
incident: Incident;
|
||||
}
|
||||
|
||||
interface Comment {
|
||||
text: { format: string; content: string };
|
||||
export interface UpdateIncidentParams {
|
||||
incidentId: string;
|
||||
incident: Incident;
|
||||
}
|
||||
|
||||
interface CreateIncidentRequestArgs extends CreateIncidentBasicRequestArgs {
|
||||
comments?: Comment[];
|
||||
export interface CreateCommentParams {
|
||||
incidentId: string;
|
||||
comment: Comment;
|
||||
}
|
||||
|
||||
export type GetIncidentTypesResponse = Array<{ id: string; name: string }>;
|
||||
export type GetSeverityResponse = Array<{ id: string; name: string }>;
|
||||
|
||||
export interface ExternalService {
|
||||
getIncident: (id: string) => Promise<ExternalServiceParams | undefined>;
|
||||
createIncident: (params: CreateIncidentParams) => Promise<ExternalServiceIncidentResponse>;
|
||||
updateIncident: (params: UpdateIncidentParams) => Promise<ExternalServiceIncidentResponse>;
|
||||
createComment: (params: CreateCommentParams) => Promise<ExternalServiceCommentResponse>;
|
||||
getIncidentTypes: () => Promise<GetIncidentTypesResponse>;
|
||||
getSeverity: () => Promise<GetSeverityResponse>;
|
||||
}
|
||||
|
||||
export interface PushToServiceApiParams extends ExecutorSubActionPushParams {
|
||||
externalObject: Record<string, any>;
|
||||
}
|
||||
|
||||
export type ExecutorSubActionGetIncidentTypesParams = TypeOf<
|
||||
typeof ExecutorSubActionGetIncidentTypesParamsSchema
|
||||
>;
|
||||
|
||||
export type ExecutorSubActionGetSeverityParams = TypeOf<
|
||||
typeof ExecutorSubActionGetSeverityParamsSchema
|
||||
>;
|
||||
|
||||
export interface ExternalServiceApiHandlerArgs {
|
||||
externalService: ExternalService;
|
||||
mapping: Map<string, any> | null;
|
||||
}
|
||||
|
||||
export type ExecutorSubActionGetIncidentParams = TypeOf<
|
||||
typeof ExecutorSubActionGetIncidentParamsSchema
|
||||
>;
|
||||
|
||||
export type ExecutorSubActionHandshakeParams = TypeOf<
|
||||
typeof ExecutorSubActionHandshakeParamsSchema
|
||||
>;
|
||||
|
||||
export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs {
|
||||
params: PushToServiceApiParams;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export interface GetIncidentApiHandlerArgs extends ExternalServiceApiHandlerArgs {
|
||||
params: ExecutorSubActionGetIncidentParams;
|
||||
}
|
||||
|
||||
export interface HandshakeApiHandlerArgs extends ExternalServiceApiHandlerArgs {
|
||||
params: ExecutorSubActionHandshakeParams;
|
||||
}
|
||||
|
||||
export interface GetIncidentTypesHandlerArgs {
|
||||
externalService: ExternalService;
|
||||
params: ExecutorSubActionGetIncidentTypesParams;
|
||||
}
|
||||
|
||||
export interface GetSeverityHandlerArgs {
|
||||
externalService: ExternalService;
|
||||
params: ExecutorSubActionGetSeverityParams;
|
||||
}
|
||||
|
||||
export interface PushToServiceResponse extends ExternalServiceIncidentResponse {
|
||||
comments?: ExternalServiceCommentResponse[];
|
||||
}
|
||||
|
||||
export interface ExternalServiceApi {
|
||||
handshake: (args: HandshakeApiHandlerArgs) => Promise<void>;
|
||||
pushToService: (args: PushToServiceApiHandlerArgs) => Promise<PushToServiceResponse>;
|
||||
getIncident: (args: GetIncidentApiHandlerArgs) => Promise<void>;
|
||||
incidentTypes: (args: GetIncidentTypesHandlerArgs) => Promise<GetIncidentTypesResponse>;
|
||||
severity: (args: GetSeverityHandlerArgs) => Promise<GetSeverityResponse>;
|
||||
}
|
||||
|
||||
export type ResilientExecutorResultData =
|
||||
| PushToServiceResponse
|
||||
| GetIncidentTypesResponse
|
||||
| GetSeverityResponse;
|
||||
|
||||
export interface UpdateFieldText {
|
||||
text: string;
|
||||
}
|
||||
export interface UpdateFieldText {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface UpdateIdsField {
|
||||
ids: number[];
|
||||
}
|
||||
|
||||
export interface UpdateIdField {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface UpdateFieldTextArea {
|
||||
textarea: { format: 'html' | 'text'; content: string };
|
||||
|
@ -34,13 +180,24 @@ export interface UpdateFieldTextArea {
|
|||
|
||||
interface UpdateField {
|
||||
field: { name: string };
|
||||
old_value: UpdateFieldText | UpdateFieldTextArea;
|
||||
new_value: UpdateFieldText | UpdateFieldTextArea;
|
||||
old_value: UpdateFieldText | UpdateFieldTextArea | UpdateIdsField | UpdateIdField;
|
||||
new_value: UpdateFieldText | UpdateFieldTextArea | UpdateIdsField | UpdateIdField;
|
||||
}
|
||||
|
||||
export type CreateIncidentRequest = CreateIncidentRequestArgs;
|
||||
export type CreateCommentRequest = Comment;
|
||||
|
||||
export interface UpdateIncidentRequest {
|
||||
changes: UpdateField[];
|
||||
}
|
||||
|
||||
export type GetValueTextContentResponse =
|
||||
| UpdateFieldText
|
||||
| UpdateFieldTextArea
|
||||
| UpdateIdsField
|
||||
| UpdateIdField;
|
||||
|
||||
export interface CreateIncidentData {
|
||||
name: string;
|
||||
discovered_date: number;
|
||||
description?: { format: string; content: string };
|
||||
incident_type_ids?: Array<{ id: number }>;
|
||||
severity_code?: { id: number };
|
||||
}
|
||||
|
|
|
@ -4,8 +4,38 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { validateCommonConfig, validateCommonSecrets } from '../case/validators';
|
||||
import { ExternalServiceValidation } from '../case/types';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { ActionsConfigurationUtilities } from '../../actions_config';
|
||||
import {
|
||||
ResilientPublicConfigurationType,
|
||||
ResilientSecretConfigurationType,
|
||||
ExternalServiceValidation,
|
||||
} from './types';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const validateCommonConfig = (
|
||||
configurationUtilities: ActionsConfigurationUtilities,
|
||||
configObject: ResilientPublicConfigurationType
|
||||
) => {
|
||||
if (
|
||||
configObject.incidentConfiguration !== null &&
|
||||
isEmpty(configObject.incidentConfiguration.mapping)
|
||||
) {
|
||||
return i18n.MAPPING_EMPTY;
|
||||
}
|
||||
|
||||
try {
|
||||
configurationUtilities.ensureUriAllowed(configObject.apiUrl);
|
||||
} catch (allowedListError) {
|
||||
return i18n.ALLOWED_HOSTS_ERROR(allowedListError.message);
|
||||
}
|
||||
};
|
||||
|
||||
export const validateCommonSecrets = (
|
||||
configurationUtilities: ActionsConfigurationUtilities,
|
||||
secrets: ResilientSecretConfigurationType
|
||||
) => {};
|
||||
|
||||
export const validate: ExternalServiceValidation = {
|
||||
config: validateCommonConfig,
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { flow } from 'lodash';
|
||||
import {
|
||||
ExternalServiceParams,
|
||||
PushToServiceApiHandlerArgs,
|
||||
|
@ -12,12 +11,11 @@ import {
|
|||
ExternalServiceApi,
|
||||
PushToServiceApiParams,
|
||||
PushToServiceResponse,
|
||||
Incident,
|
||||
} from './types';
|
||||
|
||||
// TODO: to remove, need to support Case
|
||||
import { transformers } from '../case/transformers';
|
||||
import { TransformFieldsArgs, Comment, EntityInformation } from '../case/common_types';
|
||||
import { prepareFieldsForTransformation } from '../case/utils';
|
||||
import { transformFields, transformComments, prepareFieldsForTransformation } from '../case/utils';
|
||||
|
||||
const handshakeHandler = async ({
|
||||
externalService,
|
||||
|
@ -62,7 +60,7 @@ const pushToServiceHandler = async ({
|
|||
defaultPipes,
|
||||
});
|
||||
|
||||
incident = transformFields({
|
||||
incident = transformFields<PushToServiceApiParams, ExternalServiceParams, Incident>({
|
||||
params,
|
||||
fields,
|
||||
currentIncident,
|
||||
|
@ -117,47 +115,6 @@ const pushToServiceHandler = async ({
|
|||
return res;
|
||||
};
|
||||
|
||||
export const transformFields = ({
|
||||
params,
|
||||
fields,
|
||||
currentIncident,
|
||||
}: TransformFieldsArgs<PushToServiceApiParams, ExternalServiceParams>): Record<string, string> => {
|
||||
return fields.reduce((prev, cur) => {
|
||||
const transform = flow(...cur.pipes.map((p) => transformers[p]));
|
||||
return {
|
||||
...prev,
|
||||
[cur.key]: transform({
|
||||
value: cur.value,
|
||||
date: params.updatedAt ?? params.createdAt,
|
||||
user: getEntity(params),
|
||||
previousValue: currentIncident ? currentIncident[cur.key] : '',
|
||||
}).value,
|
||||
};
|
||||
}, {});
|
||||
};
|
||||
|
||||
export const transformComments = (comments: Comment[], pipes: string[]): Comment[] => {
|
||||
return comments.map((c) => ({
|
||||
...c,
|
||||
comment: flow(...pipes.map((p) => transformers[p]))({
|
||||
value: c.comment,
|
||||
date: c.updatedAt ?? c.createdAt,
|
||||
user: getEntity(c),
|
||||
}).value,
|
||||
}));
|
||||
};
|
||||
|
||||
export const getEntity = (entity: EntityInformation): string =>
|
||||
(entity.updatedBy != null
|
||||
? entity.updatedBy.fullName
|
||||
? entity.updatedBy.fullName
|
||||
: entity.updatedBy.username
|
||||
: entity.createdBy != null
|
||||
? entity.createdBy.fullName
|
||||
? entity.createdBy.fullName
|
||||
: entity.createdBy.username
|
||||
: '') ?? '';
|
||||
|
||||
export const api: ExternalServiceApi = {
|
||||
handshake: handshakeHandler,
|
||||
pushToService: pushToServiceHandler,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { ExternalService, PushToServiceApiParams, ExecutorSubActionPushParams } from './types';
|
||||
import { MapRecord } from '../case/common_types';
|
||||
import { MapRecord } from '../case/types';
|
||||
|
||||
const createMock = (): jest.Mocked<ExternalService> => {
|
||||
const service = {
|
||||
|
|
|
@ -5,11 +5,7 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import {
|
||||
CommentSchema,
|
||||
EntityInformation,
|
||||
IncidentConfigurationSchema,
|
||||
} from '../case/common_schema';
|
||||
import { CommentSchema, EntityInformation, IncidentConfigurationSchema } from '../case/schema';
|
||||
|
||||
export const ExternalIncidentServiceConfiguration = {
|
||||
apiUrl: schema.string(),
|
||||
|
|
|
@ -16,8 +16,8 @@ import {
|
|||
ExecutorSubActionHandshakeParamsSchema,
|
||||
} from './schema';
|
||||
import { ActionsConfigurationUtilities } from '../../actions_config';
|
||||
import { ExternalServiceCommentResponse } from '../case/common_types';
|
||||
import { IncidentConfigurationSchema } from '../case/common_schema';
|
||||
import { ExternalServiceCommentResponse } from '../case/types';
|
||||
import { IncidentConfigurationSchema } from '../case/schema';
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
|
||||
export type ServiceNowPublicConfigurationType = TypeOf<
|
||||
|
@ -82,6 +82,13 @@ export type ExecutorSubActionHandshakeParams = TypeOf<
|
|||
typeof ExecutorSubActionHandshakeParamsSchema
|
||||
>;
|
||||
|
||||
export type Incident = Pick<
|
||||
ExecutorSubActionPushParams,
|
||||
'description' | 'severity' | 'urgency' | 'impact'
|
||||
> & {
|
||||
short_description: string;
|
||||
};
|
||||
|
||||
export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs {
|
||||
params: PushToServiceApiParams;
|
||||
secrets: Record<string, unknown>;
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
SUPPORTED_CONNECTORS,
|
||||
SERVICENOW_ACTION_TYPE_ID,
|
||||
JIRA_ACTION_TYPE_ID,
|
||||
RESILIENT_ACTION_TYPE_ID,
|
||||
} from '../../../../../common/constants';
|
||||
|
||||
/*
|
||||
|
@ -37,8 +38,12 @@ export function initCaseConfigureGetActionConnector({ caseService, router }: Rou
|
|||
(action) =>
|
||||
SUPPORTED_CONNECTORS.includes(action.actionTypeId) &&
|
||||
// Need this filtering temporary to display only Case owned ServiceNow connectors
|
||||
(![SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID].includes(action.actionTypeId) ||
|
||||
([SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID].includes(action.actionTypeId) &&
|
||||
(![SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID, RESILIENT_ACTION_TYPE_ID].includes(
|
||||
action.actionTypeId
|
||||
) ||
|
||||
([SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID, RESILIENT_ACTION_TYPE_ID].includes(
|
||||
action.actionTypeId
|
||||
) &&
|
||||
action.config?.isCaseOwned === true))
|
||||
);
|
||||
return response.ok({ body: results });
|
||||
|
|
|
@ -77,7 +77,7 @@ export const connectorsMock: Connector[] = [
|
|||
name: 'Jira',
|
||||
config: {
|
||||
apiUrl: 'https://instance.atlassian.ne',
|
||||
casesConfiguration: {
|
||||
incidentConfiguration: {
|
||||
mapping: [
|
||||
{
|
||||
source: 'title',
|
||||
|
|
|
@ -1,157 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import { isEmpty, get } from 'lodash/fp';
|
||||
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { ActionConnectorFieldsProps } from '../../../../../../../triggers_actions_ui/public/types';
|
||||
import { FieldMapping } from '../../../../../cases/components/configure_cases/field_mapping';
|
||||
|
||||
import { CasesConfigurationMapping } from '../../../../../cases/containers/configure/types';
|
||||
|
||||
import * as i18n from '../../translations';
|
||||
import { ActionConnector, ConnectorFlyoutHOCProps } from '../../types';
|
||||
import { createDefaultMapping } from '../../utils';
|
||||
import { connectorsConfiguration } from '../../config';
|
||||
|
||||
export const withConnectorFlyout = <T extends ActionConnector>({
|
||||
ConnectorFormComponent,
|
||||
connectorActionTypeId,
|
||||
secretKeys = [],
|
||||
configKeys = [],
|
||||
}: ConnectorFlyoutHOCProps<T>) => {
|
||||
const ConnectorFlyout: React.FC<ActionConnectorFieldsProps<T>> = ({
|
||||
action,
|
||||
editActionConfig,
|
||||
editActionSecrets,
|
||||
errors,
|
||||
}) => {
|
||||
/* We do not provide defaults values to the fields (like empty string for apiUrl) intentionally.
|
||||
* If we do, errors will be shown the first time the flyout is open even though the user did not
|
||||
* interact with the form. Also, we would like to show errors for empty fields provided by the user.
|
||||
/*/
|
||||
const { apiUrl, casesConfiguration: { mapping = [] } = {} } = action.config;
|
||||
const configKeysWithDefault = [...configKeys, 'apiUrl'];
|
||||
|
||||
const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl != null;
|
||||
|
||||
/**
|
||||
* We need to distinguish between the add flyout and the edit flyout.
|
||||
* useEffect will run only once on component mount.
|
||||
* This guarantees that the function below will run only once.
|
||||
* On the first render of the component the apiUrl can be either undefined or filled.
|
||||
* If it is filled then we are on the edit flyout. Otherwise we are on the add flyout.
|
||||
*/
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEmpty(apiUrl)) {
|
||||
secretKeys.forEach((key: string) => editActionSecrets(key, ''));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
if (isEmpty(mapping)) {
|
||||
editActionConfig('casesConfiguration', {
|
||||
...action.config.casesConfiguration,
|
||||
mapping: createDefaultMapping(connectorsConfiguration[connectorActionTypeId].fields),
|
||||
});
|
||||
}
|
||||
|
||||
const handleOnChangeActionConfig = useCallback(
|
||||
(key: string, value: string) => editActionConfig(key, value),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
const handleOnBlurActionConfig = useCallback(
|
||||
(key: string) => {
|
||||
if (configKeysWithDefault.includes(key) && get(key, action.config) == null) {
|
||||
editActionConfig(key, '');
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[action.config]
|
||||
);
|
||||
|
||||
const handleOnChangeSecretConfig = useCallback(
|
||||
(key: string, value: string) => editActionSecrets(key, value),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
const handleOnBlurSecretConfig = useCallback(
|
||||
(key: string) => {
|
||||
if (secretKeys.includes(key) && get(key, action.secrets) == null) {
|
||||
editActionSecrets(key, '');
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[action.secrets]
|
||||
);
|
||||
|
||||
const handleOnChangeMappingConfig = useCallback(
|
||||
(newMapping: CasesConfigurationMapping[]) =>
|
||||
editActionConfig('casesConfiguration', {
|
||||
...action.config.casesConfiguration,
|
||||
mapping: newMapping,
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[action.config]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="apiUrl"
|
||||
fullWidth
|
||||
error={errors.apiUrl}
|
||||
isInvalid={isApiUrlInvalid}
|
||||
label={i18n.API_URL_LABEL}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={isApiUrlInvalid}
|
||||
name="apiUrl"
|
||||
value={apiUrl || ''} // Needed to prevent uncontrolled input error when value is undefined
|
||||
data-test-subj="apiUrlFromInput"
|
||||
placeholder="https://<site-url>"
|
||||
onChange={(evt) => handleOnChangeActionConfig('apiUrl', evt.target.value)}
|
||||
onBlur={handleOnBlurActionConfig.bind(null, 'apiUrl')}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<ConnectorFormComponent
|
||||
errors={errors}
|
||||
action={action}
|
||||
onChangeSecret={handleOnChangeSecretConfig}
|
||||
onBlurSecret={handleOnBlurSecretConfig}
|
||||
onChangeConfig={handleOnChangeActionConfig}
|
||||
onBlurConfig={handleOnBlurActionConfig}
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<FieldMapping
|
||||
disabled={true}
|
||||
connectorActionTypeId={connectorActionTypeId}
|
||||
mapping={mapping as CasesConfigurationMapping[]}
|
||||
onChangeMapping={handleOnChangeMappingConfig}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return ConnectorFlyout;
|
||||
};
|
|
@ -9,12 +9,12 @@
|
|||
import {
|
||||
ServiceNowConnectorConfiguration,
|
||||
JiraConnectorConfiguration,
|
||||
ResilientConnectorConfiguration,
|
||||
} from '../../../../../triggers_actions_ui/public/common';
|
||||
import { connector as resilientConnectorConfig } from './resilient/config';
|
||||
import { ConnectorConfiguration } from './types';
|
||||
|
||||
export const connectorsConfiguration: Record<string, ConnectorConfiguration> = {
|
||||
'.servicenow': ServiceNowConnectorConfiguration as ConnectorConfiguration,
|
||||
'.jira': JiraConnectorConfiguration as ConnectorConfiguration,
|
||||
'.resilient': resilientConnectorConfig,
|
||||
'.resilient': ResilientConnectorConfiguration as ConnectorConfiguration,
|
||||
};
|
||||
|
|
|
@ -1,114 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import {
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiFieldPassword,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { ConnectorFlyoutFormProps } from '../types';
|
||||
import { ResilientActionConnector } from './types';
|
||||
import { withConnectorFlyout } from '../components/connector_flyout';
|
||||
|
||||
const resilientConnectorForm: React.FC<ConnectorFlyoutFormProps<ResilientActionConnector>> = ({
|
||||
errors,
|
||||
action,
|
||||
onChangeSecret,
|
||||
onBlurSecret,
|
||||
onChangeConfig,
|
||||
onBlurConfig,
|
||||
}) => {
|
||||
const { orgId } = action.config;
|
||||
const { apiKeyId, apiKeySecret } = action.secrets;
|
||||
const isOrgIdInvalid: boolean = errors.orgId.length > 0 && orgId != null;
|
||||
const isApiKeyIdInvalid: boolean = errors.apiKeyId.length > 0 && apiKeyId != null;
|
||||
const isApiKeySecretInvalid: boolean = errors.apiKeySecret.length > 0 && apiKeySecret != null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="connector-resilient-org-id"
|
||||
fullWidth
|
||||
error={errors.orgId}
|
||||
isInvalid={isOrgIdInvalid}
|
||||
label={i18n.RESILIENT_PROJECT_KEY_LABEL}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={isOrgIdInvalid}
|
||||
name="connector-resilient-project-key"
|
||||
value={orgId || ''} // Needed to prevent uncontrolled input error when value is undefined
|
||||
data-test-subj="connector-resilient-project-key-form-input"
|
||||
onChange={(evt) => onChangeConfig('orgId', evt.target.value)}
|
||||
onBlur={() => onBlurConfig('orgId')}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="connector-resilient-apiKeyId"
|
||||
fullWidth
|
||||
error={errors.apiKeyId}
|
||||
isInvalid={isApiKeyIdInvalid}
|
||||
label={i18n.RESILIENT_API_KEY_ID_LABEL}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={isApiKeyIdInvalid}
|
||||
name="connector-resilient-apiKeyId"
|
||||
value={apiKeyId || ''} // Needed to prevent uncontrolled input error when value is undefined
|
||||
data-test-subj="connector-resilient-apiKeyId-form-input"
|
||||
onChange={(evt) => onChangeSecret('apiKeyId', evt.target.value)}
|
||||
onBlur={() => onBlurSecret('apiKeyId')}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="connector-resilient-apiKeySecret"
|
||||
fullWidth
|
||||
error={errors.apiKeySecret}
|
||||
isInvalid={isApiKeySecretInvalid}
|
||||
label={i18n.RESILIENT_API_KEY_SECRET_LABEL}
|
||||
>
|
||||
<EuiFieldPassword
|
||||
fullWidth
|
||||
isInvalid={isApiKeySecretInvalid}
|
||||
name="connector-resilient-apiKeySecret"
|
||||
value={apiKeySecret || ''} // Needed to prevent uncontrolled input error when value is undefined
|
||||
data-test-subj="connector-resilient-apiKeySecret-form-input"
|
||||
onChange={(evt) => onChangeSecret('apiKeySecret', evt.target.value)}
|
||||
onBlur={() => onBlurSecret('apiKeySecret')}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const resilientConnectorFlyout = withConnectorFlyout<ResilientActionConnector>({
|
||||
ConnectorFormComponent: resilientConnectorForm,
|
||||
secretKeys: ['apiKeyId', 'apiKeySecret'],
|
||||
configKeys: ['orgId'],
|
||||
connectorActionTypeId: '.resilient',
|
||||
});
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { resilientConnectorFlyout as default };
|
|
@ -1,54 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { lazy } from 'react';
|
||||
import {
|
||||
ValidationResult,
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
} from '../../../../../../triggers_actions_ui/public/types';
|
||||
|
||||
import { connector } from './config';
|
||||
import { createActionType } from '../utils';
|
||||
import logo from './logo.svg';
|
||||
import { ResilientActionConnector } from './types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface Errors {
|
||||
orgId: string[];
|
||||
apiKeyId: string[];
|
||||
apiKeySecret: string[];
|
||||
}
|
||||
|
||||
const validateConnector = (action: ResilientActionConnector): ValidationResult => {
|
||||
const errors: Errors = {
|
||||
orgId: [],
|
||||
apiKeyId: [],
|
||||
apiKeySecret: [],
|
||||
};
|
||||
|
||||
if (!action.config.orgId) {
|
||||
errors.orgId = [...errors.orgId, i18n.RESILIENT_PROJECT_KEY_REQUIRED];
|
||||
}
|
||||
|
||||
if (!action.secrets.apiKeyId) {
|
||||
errors.apiKeyId = [...errors.apiKeyId, i18n.RESILIENT_API_KEY_ID_REQUIRED];
|
||||
}
|
||||
|
||||
if (!action.secrets.apiKeySecret) {
|
||||
errors.apiKeySecret = [...errors.apiKeySecret, i18n.RESILIENT_API_KEY_SECRET_REQUIRED];
|
||||
}
|
||||
|
||||
return { errors };
|
||||
};
|
||||
|
||||
export const getActionType = createActionType({
|
||||
id: connector.id,
|
||||
iconClass: logo,
|
||||
selectMessage: i18n.RESILIENT_DESC,
|
||||
actionTypeTitle: connector.name,
|
||||
validateConnector,
|
||||
actionConnectorFields: lazy(() => import('./flyout')),
|
||||
});
|
|
@ -1,72 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export * from '../translations';
|
||||
|
||||
export const RESILIENT_DESC = i18n.translate(
|
||||
'xpack.securitySolution.case.connectors.resilient.selectMessageText',
|
||||
{
|
||||
defaultMessage: 'Push or update Security case data to a new issue in Resilient',
|
||||
}
|
||||
);
|
||||
|
||||
export const RESILIENT_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.case.connectors.resilient.actionTypeTitle',
|
||||
{
|
||||
defaultMessage: 'IBM Resilient',
|
||||
}
|
||||
);
|
||||
|
||||
export const RESILIENT_PROJECT_KEY_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.case.connectors.resilient.orgId',
|
||||
{
|
||||
defaultMessage: 'Organization ID',
|
||||
}
|
||||
);
|
||||
|
||||
export const RESILIENT_PROJECT_KEY_REQUIRED = i18n.translate(
|
||||
'xpack.securitySolution.case.connectors.resilient.requiredOrgIdTextField',
|
||||
{
|
||||
defaultMessage: 'Organization ID is required',
|
||||
}
|
||||
);
|
||||
|
||||
export const RESILIENT_API_KEY_ID_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.case.connectors.resilient.apiKeyId',
|
||||
{
|
||||
defaultMessage: 'API key ID',
|
||||
}
|
||||
);
|
||||
|
||||
export const RESILIENT_API_KEY_ID_REQUIRED = i18n.translate(
|
||||
'xpack.securitySolution.case.connectors.resilient.requiredApiKeyIdTextField',
|
||||
{
|
||||
defaultMessage: 'API key ID is required',
|
||||
}
|
||||
);
|
||||
|
||||
export const RESILIENT_API_KEY_SECRET_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.case.connectors.resilient.apiKeySecret',
|
||||
{
|
||||
defaultMessage: 'API key secret',
|
||||
}
|
||||
);
|
||||
|
||||
export const RESILIENT_API_KEY_SECRET_REQUIRED = i18n.translate(
|
||||
'xpack.securitySolution.case.connectors.resilient.requiredApiKeySecretTextField',
|
||||
{
|
||||
defaultMessage: 'API key secret is required',
|
||||
}
|
||||
);
|
||||
|
||||
export const MAPPING_FIELD_NAME = i18n.translate(
|
||||
'xpack.securitySolution.case.configureCases.mappingFieldName',
|
||||
{
|
||||
defaultMessage: 'Name',
|
||||
}
|
||||
);
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-restricted-imports */
|
||||
/* eslint-disable @kbn/eslint/no-restricted-paths */
|
||||
|
||||
import {
|
||||
ResilientPublicConfigurationType,
|
||||
ResilientSecretConfigurationType,
|
||||
} from '../../../../../../actions/server/builtin_action_types/resilient/types';
|
||||
|
||||
export { ResilientFieldsType } from '../../../../../../case/common/api/connectors';
|
||||
|
||||
export * from '../types';
|
||||
|
||||
export interface ResilientActionConnector {
|
||||
config: ResilientPublicConfigurationType;
|
||||
secrets: ResilientSecretConfigurationType;
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const API_URL_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.case.connectors.common.apiUrlTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'URL',
|
||||
}
|
||||
);
|
||||
|
||||
export const API_URL_REQUIRED = i18n.translate(
|
||||
'xpack.securitySolution.case.connectors.common.requiredApiUrlTextField',
|
||||
{
|
||||
defaultMessage: 'URL is required',
|
||||
}
|
||||
);
|
||||
|
||||
export const API_URL_INVALID = i18n.translate(
|
||||
'xpack.securitySolution.case.connectors.common.invalidApiUrlTextField',
|
||||
{
|
||||
defaultMessage: 'URL is invalid',
|
||||
}
|
||||
);
|
||||
|
||||
export const USERNAME_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.case.connectors.common.usernameTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Username',
|
||||
}
|
||||
);
|
||||
|
||||
export const USERNAME_REQUIRED = i18n.translate(
|
||||
'xpack.securitySolution.case.connectors.common.requiredUsernameTextField',
|
||||
{
|
||||
defaultMessage: 'Username is required',
|
||||
}
|
||||
);
|
||||
|
||||
export const PASSWORD_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.case.connectors.common.passwordTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Password',
|
||||
}
|
||||
);
|
||||
|
||||
export const PASSWORD_REQUIRED = i18n.translate(
|
||||
'xpack.securitySolution.case.connectors.common.requiredPasswordTextField',
|
||||
{
|
||||
defaultMessage: 'Password is required',
|
||||
}
|
||||
);
|
||||
|
||||
export const API_TOKEN_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.case.connectors.common.apiTokenTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'API token',
|
||||
}
|
||||
);
|
||||
|
||||
export const API_TOKEN_REQUIRED = i18n.translate(
|
||||
'xpack.securitySolution.case.connectors.common.requiredApiTokenTextField',
|
||||
{
|
||||
defaultMessage: 'API token is required',
|
||||
}
|
||||
);
|
||||
|
||||
export const EMAIL_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.case.connectors.common.emailTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Email',
|
||||
}
|
||||
);
|
||||
|
||||
export const EMAIL_REQUIRED = i18n.translate(
|
||||
'xpack.securitySolution.case.connectors.common.requiredEmailTextField',
|
||||
{
|
||||
defaultMessage: 'Email is required',
|
||||
}
|
||||
);
|
||||
|
||||
export const MAPPING_FIELD_DESC = i18n.translate(
|
||||
'xpack.securitySolution.case.configureCases.mappingFieldDescription',
|
||||
{
|
||||
defaultMessage: 'Description',
|
||||
}
|
||||
);
|
||||
|
||||
export const MAPPING_FIELD_COMMENTS = i18n.translate(
|
||||
'xpack.securitySolution.case.configureCases.mappingFieldComments',
|
||||
{
|
||||
defaultMessage: 'Comments',
|
||||
}
|
||||
);
|
|
@ -4,12 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-restricted-imports */
|
||||
/* eslint-disable @kbn/eslint/no-restricted-paths */
|
||||
|
||||
import { ActionType } from '../../../../../triggers_actions_ui/public';
|
||||
import { IErrorObject } from '../../../../../triggers_actions_ui/public/types';
|
||||
import { ExternalIncidentServiceConfiguration } from '../../../../../actions/server/builtin_action_types/case/types';
|
||||
|
||||
import {
|
||||
ActionType as ThirdPartySupportedActions,
|
||||
|
@ -29,34 +24,3 @@ export interface ConnectorConfiguration extends ActionType {
|
|||
logo: string;
|
||||
fields: Record<string, ThirdPartyField>;
|
||||
}
|
||||
|
||||
export interface ActionConnector {
|
||||
config: ExternalIncidentServiceConfiguration;
|
||||
secrets: {};
|
||||
}
|
||||
|
||||
export interface ActionConnectorParams {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ActionConnectorValidationErrors {
|
||||
apiUrl: string[];
|
||||
}
|
||||
|
||||
export type Optional<T, K extends keyof T> = Omit<T, K> & Partial<T>;
|
||||
|
||||
export interface ConnectorFlyoutFormProps<T> {
|
||||
errors: IErrorObject;
|
||||
action: T;
|
||||
onChangeSecret: (key: string, value: string) => void;
|
||||
onBlurSecret: (key: string) => void;
|
||||
onChangeConfig: (key: string, value: string) => void;
|
||||
onBlurConfig: (key: string) => void;
|
||||
}
|
||||
|
||||
export interface ConnectorFlyoutHOCProps<T> {
|
||||
ConnectorFormComponent: React.FC<ConnectorFlyoutFormProps<T>>;
|
||||
connectorActionTypeId: string;
|
||||
configKeys?: string[];
|
||||
secretKeys?: string[];
|
||||
}
|
||||
|
|
|
@ -4,63 +4,9 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
ActionTypeModel,
|
||||
ValidationResult,
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
} from '../../../../../triggers_actions_ui/public/types';
|
||||
|
||||
import {
|
||||
ActionConnector,
|
||||
ActionConnectorParams,
|
||||
ActionConnectorValidationErrors,
|
||||
Optional,
|
||||
ThirdPartyField,
|
||||
} from './types';
|
||||
import { isUrlInvalid } from './validators';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { CasesConfigurationMapping } from '../../../cases/containers/configure/types';
|
||||
|
||||
export const createActionType = ({
|
||||
id,
|
||||
actionTypeTitle,
|
||||
selectMessage,
|
||||
iconClass,
|
||||
validateConnector,
|
||||
validateParams = connectorParamsValidator,
|
||||
actionConnectorFields,
|
||||
actionParamsFields = null,
|
||||
}: Optional<ActionTypeModel, 'validateParams' | 'actionParamsFields'>) => (): ActionTypeModel => {
|
||||
return {
|
||||
id,
|
||||
iconClass,
|
||||
selectMessage,
|
||||
actionTypeTitle,
|
||||
validateConnector: (action: ActionConnector): ValidationResult => {
|
||||
const errors: ActionConnectorValidationErrors = {
|
||||
apiUrl: [],
|
||||
};
|
||||
|
||||
if (!action.config.apiUrl) {
|
||||
errors.apiUrl = [...errors.apiUrl, i18n.API_URL_REQUIRED];
|
||||
}
|
||||
|
||||
if (isUrlInvalid(action.config.apiUrl)) {
|
||||
errors.apiUrl = [...errors.apiUrl, i18n.API_URL_INVALID];
|
||||
}
|
||||
|
||||
return { errors: { ...errors, ...validateConnector(action).errors } };
|
||||
},
|
||||
validateParams,
|
||||
actionConnectorFields,
|
||||
actionParamsFields,
|
||||
};
|
||||
};
|
||||
|
||||
const connectorParamsValidator = (actionParams: ActionConnectorParams): ValidationResult => {
|
||||
return { errors: {} };
|
||||
};
|
||||
import { ThirdPartyField } from './types';
|
||||
|
||||
export const createDefaultMapping = (
|
||||
fields: Record<string, ThirdPartyField>
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { isUrlInvalid } from '../../utils/validators';
|
|
@ -21,7 +21,6 @@ import {
|
|||
import { Storage } from '../../../../src/plugins/kibana_utils/public';
|
||||
import { initTelemetry } from './common/lib/telemetry';
|
||||
import { KibanaServices } from './common/lib/kibana/services';
|
||||
import { resilientActionType } from './common/lib/connectors';
|
||||
import {
|
||||
PluginSetup,
|
||||
PluginStart,
|
||||
|
@ -96,8 +95,6 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
});
|
||||
}
|
||||
|
||||
plugins.triggers_actions_ui.actionTypeRegistry.register(resilientActionType());
|
||||
|
||||
const mountSecurityFactory = async () => {
|
||||
const storage = new Storage(localStorage);
|
||||
const [coreStart, startPlugins] = await core.getStartServices();
|
||||
|
|
|
@ -15107,9 +15107,6 @@
|
|||
"xpack.securitySolution.case.configureCases.incidentManagementSystemDesc": "オプションとして、セキュリティケースを選択した外部のインシデント管理システムに接続できます。そうすると、選択したサードパーティシステム内でケースデータをインシデントとしてプッシュできます。",
|
||||
"xpack.securitySolution.case.configureCases.incidentManagementSystemLabel": "インシデント管理システム",
|
||||
"xpack.securitySolution.case.configureCases.incidentManagementSystemTitle": "外部のインシデント管理システムに接続",
|
||||
"xpack.securitySolution.case.configureCases.mappingFieldComments": "コメント",
|
||||
"xpack.securitySolution.case.configureCases.mappingFieldDescription": "説明",
|
||||
"xpack.securitySolution.case.configureCases.mappingFieldName": "名前",
|
||||
"xpack.securitySolution.case.configureCases.mappingFieldNotMapped": "マップされません",
|
||||
"xpack.securitySolution.case.configureCases.noConnector": "コネクターを選択していません",
|
||||
"xpack.securitySolution.case.configureCases.updateConnector": "コネクターを更新",
|
||||
|
@ -15123,25 +15120,6 @@
|
|||
"xpack.securitySolution.case.confirmDeleteCase.deleteCases": "ケースを削除",
|
||||
"xpack.securitySolution.case.confirmDeleteCase.deleteTitle": "「{caseTitle}」を削除",
|
||||
"xpack.securitySolution.case.confirmDeleteCase.selectedCases": "選択したケースを削除",
|
||||
"xpack.securitySolution.case.connectors.common.apiTokenTextFieldLabel": "APIトークン",
|
||||
"xpack.securitySolution.case.connectors.common.apiUrlTextFieldLabel": "URL",
|
||||
"xpack.securitySolution.case.connectors.common.emailTextFieldLabel": "メール",
|
||||
"xpack.securitySolution.case.connectors.common.invalidApiUrlTextField": "URLが無効です",
|
||||
"xpack.securitySolution.case.connectors.common.passwordTextFieldLabel": "パスワード",
|
||||
"xpack.securitySolution.case.connectors.common.requiredApiTokenTextField": "APIトークンが必要です",
|
||||
"xpack.securitySolution.case.connectors.common.requiredApiUrlTextField": "URLが必要です",
|
||||
"xpack.securitySolution.case.connectors.common.requiredEmailTextField": "電子メールが必要です",
|
||||
"xpack.securitySolution.case.connectors.common.requiredPasswordTextField": "パスワードが必要です",
|
||||
"xpack.securitySolution.case.connectors.common.requiredUsernameTextField": "ユーザー名が必要です",
|
||||
"xpack.securitySolution.case.connectors.common.usernameTextFieldLabel": "ユーザー名",
|
||||
"xpack.securitySolution.case.connectors.resilient.actionTypeTitle": "IBM Resilient",
|
||||
"xpack.securitySolution.case.connectors.resilient.apiKeyId": "APIキーID",
|
||||
"xpack.securitySolution.case.connectors.resilient.apiKeySecret": "APIキーシークレット",
|
||||
"xpack.securitySolution.case.connectors.resilient.orgId": "組織ID",
|
||||
"xpack.securitySolution.case.connectors.resilient.requiredApiKeyIdTextField": "APIキーIDが必要です",
|
||||
"xpack.securitySolution.case.connectors.resilient.requiredApiKeySecretTextField": "APIキーシークレットが必要です",
|
||||
"xpack.securitySolution.case.connectors.resilient.requiredOrgIdTextField": "組織IDが必要です",
|
||||
"xpack.securitySolution.case.connectors.resilient.selectMessageText": "Resilientでセキュリティケースデータを更新するか、新しいインシデントにプッシュ",
|
||||
"xpack.securitySolution.case.createCase.descriptionFieldRequiredError": "説明が必要です。",
|
||||
"xpack.securitySolution.case.createCase.fieldTagsHelpText": "このケースの1つ以上のカスタム識別タグを入力します。新しいタグを開始するには、各タグの後でEnterを押します。",
|
||||
"xpack.securitySolution.case.createCase.titleFieldRequiredError": "タイトルが必要です。",
|
||||
|
|
|
@ -15116,9 +15116,6 @@
|
|||
"xpack.securitySolution.case.configureCases.incidentManagementSystemDesc": "您可能会根据需要将 Security 案例连接到选择的外部事件管理系统。这将允许您将案例数据作为事件推送到所选第三方系统。",
|
||||
"xpack.securitySolution.case.configureCases.incidentManagementSystemLabel": "事件管理系统",
|
||||
"xpack.securitySolution.case.configureCases.incidentManagementSystemTitle": "连接到外部事件管理系统",
|
||||
"xpack.securitySolution.case.configureCases.mappingFieldComments": "注释",
|
||||
"xpack.securitySolution.case.configureCases.mappingFieldDescription": "描述",
|
||||
"xpack.securitySolution.case.configureCases.mappingFieldName": "名称",
|
||||
"xpack.securitySolution.case.configureCases.mappingFieldNotMapped": "未映射",
|
||||
"xpack.securitySolution.case.configureCases.noConnector": "未选择连接器",
|
||||
"xpack.securitySolution.case.configureCases.updateConnector": "更新连接器",
|
||||
|
@ -15132,25 +15129,6 @@
|
|||
"xpack.securitySolution.case.confirmDeleteCase.deleteCases": "删除案例",
|
||||
"xpack.securitySolution.case.confirmDeleteCase.deleteTitle": "删除“{caseTitle}”",
|
||||
"xpack.securitySolution.case.confirmDeleteCase.selectedCases": "删除选定案例",
|
||||
"xpack.securitySolution.case.connectors.common.apiTokenTextFieldLabel": "API 令牌",
|
||||
"xpack.securitySolution.case.connectors.common.apiUrlTextFieldLabel": "URL",
|
||||
"xpack.securitySolution.case.connectors.common.emailTextFieldLabel": "电子邮件",
|
||||
"xpack.securitySolution.case.connectors.common.invalidApiUrlTextField": "URL 无效",
|
||||
"xpack.securitySolution.case.connectors.common.passwordTextFieldLabel": "密码",
|
||||
"xpack.securitySolution.case.connectors.common.requiredApiTokenTextField": "“API 令牌”必填",
|
||||
"xpack.securitySolution.case.connectors.common.requiredApiUrlTextField": "“URL”必填",
|
||||
"xpack.securitySolution.case.connectors.common.requiredEmailTextField": "“电子邮件”必填",
|
||||
"xpack.securitySolution.case.connectors.common.requiredPasswordTextField": "“密码”必填",
|
||||
"xpack.securitySolution.case.connectors.common.requiredUsernameTextField": "“用户名”必填",
|
||||
"xpack.securitySolution.case.connectors.common.usernameTextFieldLabel": "用户名",
|
||||
"xpack.securitySolution.case.connectors.resilient.actionTypeTitle": "IBM Resilient",
|
||||
"xpack.securitySolution.case.connectors.resilient.apiKeyId": "API 密钥 ID",
|
||||
"xpack.securitySolution.case.connectors.resilient.apiKeySecret": "API 密钥密码",
|
||||
"xpack.securitySolution.case.connectors.resilient.orgId": "组织 ID",
|
||||
"xpack.securitySolution.case.connectors.resilient.requiredApiKeyIdTextField": "“API 密钥 ID”必填",
|
||||
"xpack.securitySolution.case.connectors.resilient.requiredApiKeySecretTextField": "“API 密钥密码”必填",
|
||||
"xpack.securitySolution.case.connectors.resilient.requiredOrgIdTextField": "“组织 ID”必填",
|
||||
"xpack.securitySolution.case.connectors.resilient.selectMessageText": "将 Security 案例数据推送或更新到 Resilient 中的新问题",
|
||||
"xpack.securitySolution.case.createCase.descriptionFieldRequiredError": "描述必填。",
|
||||
"xpack.securitySolution.case.createCase.fieldTagsHelpText": "为此案例键入一个或多个定制识别标记。在每个标记后按 Enter 键可开始新的标记。",
|
||||
"xpack.securitySolution.case.createCase.titleFieldRequiredError": "标题必填。",
|
||||
|
|
|
@ -27,6 +27,7 @@ describe('EmailParamsFields renders', () => {
|
|||
docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="toEmailAddressInput"]').length > 0).toBeTruthy();
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="toEmailAddressInput"]').first().prop('selectedOptions')
|
||||
|
|
|
@ -13,6 +13,7 @@ describe('IndexParamsFields renders', () => {
|
|||
const actionParams = {
|
||||
documents: [{ test: 123 }],
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<ParamsFields
|
||||
actionParams={actionParams}
|
||||
|
|
|
@ -14,6 +14,7 @@ import { TypeRegistry } from '../../type_registry';
|
|||
import { ActionTypeModel } from '../../../types';
|
||||
import { getServiceNowActionType } from './servicenow';
|
||||
import { getJiraActionType } from './jira';
|
||||
import { getResilientActionType } from './resilient';
|
||||
|
||||
export function registerBuiltInActionTypes({
|
||||
actionTypeRegistry,
|
||||
|
@ -28,4 +29,5 @@ export function registerBuiltInActionTypes({
|
|||
actionTypeRegistry.register(getWebhookActionType());
|
||||
actionTypeRegistry.register(getServiceNowActionType());
|
||||
actionTypeRegistry.register(getJiraActionType());
|
||||
actionTypeRegistry.register(getResilientActionType());
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ describe('PagerDutyParamsFields renders', () => {
|
|||
group: 'group',
|
||||
class: 'test class',
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<PagerDutyParamsFields
|
||||
actionParams={actionParams}
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 getIncidentTypes({
|
||||
http,
|
||||
signal,
|
||||
connectorId,
|
||||
}: {
|
||||
http: HttpSetup;
|
||||
signal: AbortSignal;
|
||||
connectorId: string;
|
||||
}): Promise<Record<string, any>> {
|
||||
return await http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, {
|
||||
body: JSON.stringify({
|
||||
params: { subAction: 'incidentTypes', subActionParams: {} },
|
||||
}),
|
||||
signal,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getSeverity({
|
||||
http,
|
||||
signal,
|
||||
connectorId,
|
||||
}: {
|
||||
http: HttpSetup;
|
||||
signal: AbortSignal;
|
||||
connectorId: string;
|
||||
}): Promise<Record<string, any>> {
|
||||
return await http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, {
|
||||
body: JSON.stringify({
|
||||
params: { subAction: 'severity', subActionParams: {} },
|
||||
}),
|
||||
signal,
|
||||
});
|
||||
}
|
|
@ -4,14 +4,12 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ConnectorConfiguration } from './types';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import logo from './logo.svg';
|
||||
|
||||
export const connector: ConnectorConfiguration = {
|
||||
export const connectorConfiguration = {
|
||||
id: '.resilient',
|
||||
name: i18n.RESILIENT_TITLE,
|
||||
name: i18n.TITLE,
|
||||
logo,
|
||||
enabled: true,
|
||||
enabledInConfig: true,
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { getActionType as resilientActionType } from './resilient';
|
||||
export { getActionType as getResilientActionType } from './resilient';
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* 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 { TypeRegistry } from '../../../type_registry';
|
||||
import { registerBuiltInActionTypes } from '.././index';
|
||||
import { ActionTypeModel } from '../../../../types';
|
||||
import { ResilientActionConnector } from './types';
|
||||
|
||||
const ACTION_TYPE_ID = '.resilient';
|
||||
let actionTypeModel: ActionTypeModel;
|
||||
|
||||
beforeAll(() => {
|
||||
const 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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resilient connector validation', () => {
|
||||
test('connector validation succeeds when connector config is valid', () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
apiKeyId: 'email',
|
||||
apiKeySecret: 'token',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.resilient',
|
||||
isPreconfigured: false,
|
||||
name: 'resilient',
|
||||
config: {
|
||||
apiUrl: 'https://test/',
|
||||
orgId: '201',
|
||||
},
|
||||
} as ResilientActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
errors: {
|
||||
apiUrl: [],
|
||||
apiKeyId: [],
|
||||
apiKeySecret: [],
|
||||
orgId: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when connector config is not valid', () => {
|
||||
const actionConnector = ({
|
||||
secrets: {
|
||||
apiKeyId: 'user',
|
||||
},
|
||||
id: '.jira',
|
||||
actionTypeId: '.jira',
|
||||
name: 'jira',
|
||||
config: {},
|
||||
} as unknown) as ResilientActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
errors: {
|
||||
apiUrl: ['URL is required.'],
|
||||
apiKeyId: [],
|
||||
apiKeySecret: ['API key secret is required'],
|
||||
orgId: ['Organization ID is required'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resilient action params validation', () => {
|
||||
test('action params validation succeeds when action params is valid', () => {
|
||||
const actionParams = {
|
||||
subActionParams: { title: 'some title {{test}}' },
|
||||
};
|
||||
|
||||
expect(actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: { title: [] },
|
||||
});
|
||||
});
|
||||
|
||||
test('params validation fails when body is not valid', () => {
|
||||
const actionParams = {
|
||||
subActionParams: { title: '' },
|
||||
};
|
||||
|
||||
expect(actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
title: ['Title is required.'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 { lazy } from 'react';
|
||||
import { ValidationResult, ActionTypeModel } from '../../../../types';
|
||||
import { connectorConfiguration } from './config';
|
||||
import logo from './logo.svg';
|
||||
import { ResilientActionConnector, ResilientActionParams } from './types';
|
||||
import * as i18n from './translations';
|
||||
import { isValidUrl } from '../../../lib/value_validators';
|
||||
|
||||
const validateConnector = (action: ResilientActionConnector): ValidationResult => {
|
||||
const validationResult = { errors: {} };
|
||||
const errors = {
|
||||
apiUrl: new Array<string>(),
|
||||
orgId: new Array<string>(),
|
||||
apiKeyId: new Array<string>(),
|
||||
apiKeySecret: new Array<string>(),
|
||||
};
|
||||
validationResult.errors = errors;
|
||||
|
||||
if (!action.config.apiUrl) {
|
||||
errors.apiUrl = [...errors.apiUrl, i18n.API_URL_REQUIRED];
|
||||
}
|
||||
|
||||
if (action.config.apiUrl && !isValidUrl(action.config.apiUrl, 'https:')) {
|
||||
errors.apiUrl = [...errors.apiUrl, i18n.API_URL_INVALID];
|
||||
}
|
||||
|
||||
if (!action.config.orgId) {
|
||||
errors.orgId = [...errors.orgId, i18n.ORG_ID_REQUIRED];
|
||||
}
|
||||
|
||||
if (!action.secrets.apiKeyId) {
|
||||
errors.apiKeyId = [...errors.apiKeyId, i18n.API_KEY_ID_REQUIRED];
|
||||
}
|
||||
|
||||
if (!action.secrets.apiKeySecret) {
|
||||
errors.apiKeySecret = [...errors.apiKeySecret, i18n.API_KEY_SECRET_REQUIRED];
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
};
|
||||
|
||||
export function getActionType(): ActionTypeModel<ResilientActionConnector, ResilientActionParams> {
|
||||
return {
|
||||
id: connectorConfiguration.id,
|
||||
iconClass: logo,
|
||||
selectMessage: i18n.DESC,
|
||||
actionTypeTitle: connectorConfiguration.name,
|
||||
validateConnector,
|
||||
actionConnectorFields: lazy(() => import('./resilient_connectors')),
|
||||
validateParams: (actionParams: ResilientActionParams): ValidationResult => {
|
||||
const validationResult = { errors: {} };
|
||||
const errors = {
|
||||
title: new Array<string>(),
|
||||
};
|
||||
validationResult.errors = errors;
|
||||
if (actionParams.subActionParams && !actionParams.subActionParams.title?.length) {
|
||||
errors.title.push(i18n.TITLE_REQUIRED);
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
actionParamsFields: lazy(() => import('./resilient_params')),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* 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 { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { DocLinksStart } from 'kibana/public';
|
||||
import ResilientConnectorFields from './resilient_connectors';
|
||||
import { ResilientActionConnector } from './types';
|
||||
|
||||
describe('ResilientActionConnectorFields renders', () => {
|
||||
test('alerting Resilient connector fields is rendered', () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
apiKeyId: 'key',
|
||||
apiKeySecret: 'secret',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.resilient',
|
||||
isPreconfigured: false,
|
||||
name: 'resilient',
|
||||
config: {
|
||||
apiUrl: 'https://test/',
|
||||
orgId: '201',
|
||||
},
|
||||
} as ResilientActionConnector;
|
||||
const deps = {
|
||||
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart,
|
||||
};
|
||||
const wrapper = mountWithIntl(
|
||||
<ResilientConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ apiUrl: [], apiKeyId: [], apiKeySecret: [], orgId: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
docLinks={deps!.docLinks}
|
||||
readOnly={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy();
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="connector-resilient-orgId-form-input"]').length > 0
|
||||
).toBeTruthy();
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="connector-resilient-apiKeySecret-form-input"]').length > 0
|
||||
).toBeTruthy();
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="connector-resilient-apiKeySecret-form-input"]').length > 0
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('case specific Resilient connector fields is rendered', () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
apiKeyId: 'email',
|
||||
apiKeySecret: 'token',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.resilient',
|
||||
isPreconfigured: false,
|
||||
name: 'resilient',
|
||||
config: {
|
||||
apiUrl: 'https://test/',
|
||||
orgId: '201',
|
||||
},
|
||||
} as ResilientActionConnector;
|
||||
const deps = {
|
||||
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart,
|
||||
};
|
||||
const wrapper = mountWithIntl(
|
||||
<ResilientConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ apiUrl: [], apiKeyId: [], apiKeySecret: [], orgId: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
docLinks={deps!.docLinks}
|
||||
readOnly={false}
|
||||
consumer={'case'}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="case-resilient-mappings"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy();
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="connector-resilient-orgId-form-input"]').length > 0
|
||||
).toBeTruthy();
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="connector-resilient-apiKeySecret-form-input"]').length > 0
|
||||
).toBeTruthy();
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="connector-resilient-apiKeySecret-form-input"]').length > 0
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,209 @@
|
|||
/*
|
||||
* 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, { useCallback } from 'react';
|
||||
|
||||
import {
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiFieldPassword,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
import { ActionConnectorFieldsProps } from '../../../../types';
|
||||
import * as i18n from './translations';
|
||||
import { ResilientActionConnector } from './types';
|
||||
import { connectorConfiguration } from './config';
|
||||
import { FieldMapping, CasesConfigurationMapping, createDefaultMapping } from '../case_mappings';
|
||||
|
||||
const ResilientConnectorFields: React.FC<ActionConnectorFieldsProps<ResilientActionConnector>> = ({
|
||||
action,
|
||||
editActionSecrets,
|
||||
editActionConfig,
|
||||
errors,
|
||||
consumer,
|
||||
readOnly,
|
||||
docLinks,
|
||||
}) => {
|
||||
// TODO: remove incidentConfiguration later, when Case Resilient will move their fields to the level of action execution
|
||||
const { apiUrl, orgId, incidentConfiguration, isCaseOwned } = action.config;
|
||||
const mapping = incidentConfiguration ? incidentConfiguration.mapping : [];
|
||||
|
||||
const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl != null;
|
||||
|
||||
const { apiKeyId, apiKeySecret } = action.secrets;
|
||||
|
||||
const isOrgIdInvalid: boolean = errors.orgId.length > 0 && orgId != null;
|
||||
const isApiKeyInvalid: boolean = errors.apiKeyId.length > 0 && apiKeyId != null;
|
||||
const isApiKeySecretInvalid: boolean = errors.apiKeySecret.length > 0 && apiKeySecret != null;
|
||||
|
||||
// TODO: remove this block later, when Case ServiceNow will move their fields to the level of action execution
|
||||
if (consumer === 'case') {
|
||||
if (isEmpty(mapping)) {
|
||||
editActionConfig('incidentConfiguration', {
|
||||
mapping: createDefaultMapping(connectorConfiguration.fields as any),
|
||||
});
|
||||
}
|
||||
|
||||
if (!isCaseOwned) {
|
||||
editActionConfig('isCaseOwned', true);
|
||||
}
|
||||
}
|
||||
|
||||
const handleOnChangeActionConfig = useCallback(
|
||||
(key: string, value: string) => editActionConfig(key, value),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
const handleOnChangeSecretConfig = useCallback(
|
||||
(key: string, value: string) => editActionSecrets(key, value),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
const handleOnChangeMappingConfig = useCallback(
|
||||
(newMapping: CasesConfigurationMapping[]) =>
|
||||
editActionConfig('incidentConfiguration', {
|
||||
...action.config.incidentConfiguration,
|
||||
mapping: newMapping,
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[action.config]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="apiUrl"
|
||||
fullWidth
|
||||
error={errors.apiUrl}
|
||||
isInvalid={isApiUrlInvalid}
|
||||
label={i18n.API_URL_LABEL}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={isApiUrlInvalid}
|
||||
name="apiUrl"
|
||||
readOnly={readOnly}
|
||||
value={apiUrl || ''} // Needed to prevent uncontrolled input error when value is undefined
|
||||
data-test-subj="apiUrlFromInput"
|
||||
placeholder="https://<site-url>"
|
||||
onChange={(evt) => handleOnChangeActionConfig('apiUrl', evt.target.value)}
|
||||
onBlur={() => {
|
||||
if (!apiUrl) {
|
||||
editActionConfig('apiUrl', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="connector-resilient-orgId-key"
|
||||
fullWidth
|
||||
error={errors.orgId}
|
||||
isInvalid={isOrgIdInvalid}
|
||||
label={i18n.ORG_ID_LABEL}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={isOrgIdInvalid}
|
||||
name="connector-resilient-orgId"
|
||||
value={orgId || ''} // Needed to prevent uncontrolled input error when value is undefined
|
||||
data-test-subj="connector-resilient-orgId-form-input"
|
||||
onChange={(evt) => handleOnChangeActionConfig('orgId', evt.target.value)}
|
||||
onBlur={() => {
|
||||
if (!orgId) {
|
||||
editActionConfig('orgId', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="connector-resilient-apiKeyId"
|
||||
fullWidth
|
||||
error={errors.apiKeyId}
|
||||
isInvalid={isApiKeyInvalid}
|
||||
label={i18n.API_KEY_ID_LABEL}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={isApiKeyInvalid}
|
||||
readOnly={readOnly}
|
||||
name="connector-resilient-apiKeyId"
|
||||
value={apiKeyId || ''} // Needed to prevent uncontrolled input error when value is undefined
|
||||
data-test-subj="connector-resilient-apiKeyId-form-input"
|
||||
onChange={(evt) => handleOnChangeSecretConfig('apiKeyId', evt.target.value)}
|
||||
onBlur={() => {
|
||||
if (!apiKeyId) {
|
||||
editActionSecrets('apiKeyId', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="connector-resilient-apiKeySecret"
|
||||
fullWidth
|
||||
error={errors.apiKeySecret}
|
||||
isInvalid={isApiKeySecretInvalid}
|
||||
label={i18n.API_KEY_SECRET_LABEL}
|
||||
>
|
||||
<EuiFieldPassword
|
||||
fullWidth
|
||||
readOnly={readOnly}
|
||||
isInvalid={isApiKeySecretInvalid}
|
||||
name="connector-resilient-apiKeySecret"
|
||||
value={apiKeySecret || ''} // Needed to prevent uncontrolled input error when value is undefined
|
||||
data-test-subj="connector-resilient-apiKeySecret-form-input"
|
||||
onChange={(evt) => handleOnChangeSecretConfig('apiKeySecret', evt.target.value)}
|
||||
onBlur={() => {
|
||||
if (!apiKeySecret) {
|
||||
editActionSecrets('apiKeySecret', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{consumer === 'case' && ( // TODO: remove this block later, when Case Resilient will move their fields to the level of action execution
|
||||
<>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem data-test-subj="case-resilient-mappings">
|
||||
<FieldMapping
|
||||
disabled={true}
|
||||
connectorConfiguration={connectorConfiguration}
|
||||
mapping={mapping as CasesConfigurationMapping[]}
|
||||
onChangeMapping={handleOnChangeMappingConfig}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { ResilientConnectorFields as default };
|
|
@ -0,0 +1,189 @@
|
|||
/*
|
||||
* 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 { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import ResilientParamsFields from './resilient_params';
|
||||
import { DocLinksStart } from 'kibana/public';
|
||||
|
||||
import { useGetIncidentTypes } from './use_get_incident_types';
|
||||
import { useGetSeverity } from './use_get_severity';
|
||||
|
||||
jest.mock('../../../app_context', () => {
|
||||
const post = jest.fn();
|
||||
return {
|
||||
useAppDependencies: jest.fn(() => ({ http: { post } })),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('./use_get_incident_types');
|
||||
jest.mock('./use_get_severity');
|
||||
|
||||
const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock;
|
||||
const useGetSeverityMock = useGetSeverity as jest.Mock;
|
||||
|
||||
const actionParams = {
|
||||
subAction: 'pushToService',
|
||||
subActionParams: {
|
||||
title: 'title',
|
||||
description: 'some description',
|
||||
comments: [{ commentId: '1', comment: 'comment for resilient' }],
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
savedObjectId: '123',
|
||||
externalId: null,
|
||||
},
|
||||
};
|
||||
const connector = {
|
||||
secrets: {},
|
||||
config: {},
|
||||
id: 'test',
|
||||
actionTypeId: '.test',
|
||||
name: 'Test',
|
||||
isPreconfigured: false,
|
||||
};
|
||||
|
||||
describe('ResilientParamsFields renders', () => {
|
||||
const useGetIncidentTypesResponse = {
|
||||
isLoading: false,
|
||||
incidentTypes: [
|
||||
{
|
||||
id: 19,
|
||||
name: 'Malware',
|
||||
},
|
||||
{
|
||||
id: 21,
|
||||
name: 'Denial of Service',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const useGetSeverityResponse = {
|
||||
isLoading: false,
|
||||
severity: [
|
||||
{
|
||||
id: 4,
|
||||
name: 'Low',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Medium',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'High',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse);
|
||||
useGetSeverityMock.mockReturnValue(useGetSeverityResponse);
|
||||
});
|
||||
|
||||
test('all params fields are rendered', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ResilientParamsFields
|
||||
actionParams={actionParams}
|
||||
errors={{ title: [] }}
|
||||
editAction={() => {}}
|
||||
index={0}
|
||||
messageVariables={[]}
|
||||
docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart}
|
||||
actionConnector={connector}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="incidentTypeComboBox"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="severitySelect"]').first().prop('value')).toStrictEqual(
|
||||
6
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="titleInput"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="descriptionTextArea"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="commentsTextArea"]').length > 0).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it shows loading when loading incident types', () => {
|
||||
useGetIncidentTypesMock.mockReturnValue({ ...useGetIncidentTypesResponse, isLoading: true });
|
||||
const wrapper = mountWithIntl(
|
||||
<ResilientParamsFields
|
||||
actionParams={actionParams}
|
||||
errors={{ title: [] }}
|
||||
editAction={() => {}}
|
||||
index={0}
|
||||
messageVariables={[]}
|
||||
docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart}
|
||||
actionConnector={connector}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="incidentTypeComboBox"]').first().prop('isLoading')
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it shows loading when loading severity', () => {
|
||||
useGetSeverityMock.mockReturnValue({
|
||||
...useGetSeverityResponse,
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<ResilientParamsFields
|
||||
actionParams={actionParams}
|
||||
errors={{ title: [] }}
|
||||
editAction={() => {}}
|
||||
index={0}
|
||||
messageVariables={[]}
|
||||
docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart}
|
||||
actionConnector={connector}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="severitySelect"]').first().prop('isLoading')
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it disabled the fields when loading issue types', () => {
|
||||
useGetIncidentTypesMock.mockReturnValue({ ...useGetIncidentTypesResponse, isLoading: true });
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<ResilientParamsFields
|
||||
actionParams={actionParams}
|
||||
errors={{ title: [] }}
|
||||
editAction={() => {}}
|
||||
index={0}
|
||||
messageVariables={[]}
|
||||
docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart}
|
||||
actionConnector={connector}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="incidentTypeComboBox"]').first().prop('isDisabled')
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it disabled the fields when loading severity', () => {
|
||||
useGetSeverityMock.mockReturnValue({
|
||||
...useGetSeverityResponse,
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<ResilientParamsFields
|
||||
actionParams={actionParams}
|
||||
errors={{ title: [] }}
|
||||
editAction={() => {}}
|
||||
index={0}
|
||||
messageVariables={[]}
|
||||
docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart}
|
||||
actionConnector={connector}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="severitySelect"]').first().prop('disabled')).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,256 @@
|
|||
/*
|
||||
* 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, useEffect, useState } from 'react';
|
||||
import {
|
||||
EuiFormRow,
|
||||
EuiComboBox,
|
||||
EuiSelect,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
EuiComboBoxOptionOption,
|
||||
EuiSelectOption,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { ActionParamsProps } from '../../../../types';
|
||||
import { useAppDependencies } from '../../../app_context';
|
||||
import { ResilientActionParams } from './types';
|
||||
import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables';
|
||||
import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables';
|
||||
|
||||
import { useGetIncidentTypes } from './use_get_incident_types';
|
||||
import { useGetSeverity } from './use_get_severity';
|
||||
|
||||
const ResilientParamsFields: React.FunctionComponent<ActionParamsProps<ResilientActionParams>> = ({
|
||||
actionParams,
|
||||
editAction,
|
||||
index,
|
||||
errors,
|
||||
messageVariables,
|
||||
actionConnector,
|
||||
}) => {
|
||||
const [firstLoad, setFirstLoad] = useState(false);
|
||||
const { http, toastNotifications } = useAppDependencies();
|
||||
const { title, description, comments, incidentTypes, severityCode, savedObjectId } =
|
||||
actionParams.subActionParams || {};
|
||||
|
||||
const [incidentTypesComboBoxOptions, setIncidentTypesComboBoxOptions] = useState<
|
||||
Array<EuiComboBoxOptionOption<string>>
|
||||
>([]);
|
||||
|
||||
const [selectedIncidentTypesComboBoxOptions, setSelectedIncidentTypesComboBoxOptions] = useState<
|
||||
Array<EuiComboBoxOptionOption<string>>
|
||||
>([]);
|
||||
|
||||
const [severitySelectOptions, setSeveritySelectOptions] = useState<EuiSelectOption[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setFirstLoad(true);
|
||||
}, []);
|
||||
|
||||
const {
|
||||
isLoading: isLoadingIncidentTypes,
|
||||
incidentTypes: allIncidentTypes,
|
||||
} = useGetIncidentTypes({
|
||||
http,
|
||||
toastNotifications,
|
||||
actionConnector,
|
||||
});
|
||||
|
||||
const { isLoading: isLoadingSeverity, severity } = useGetSeverity({
|
||||
http,
|
||||
toastNotifications,
|
||||
actionConnector,
|
||||
});
|
||||
|
||||
const editSubActionProperty = (key: string, value: {}) => {
|
||||
const newProps = { ...actionParams.subActionParams, [key]: value };
|
||||
editAction('subActionParams', newProps, index);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const options = severity.map((s) => ({
|
||||
value: s.id.toString(),
|
||||
text: s.name,
|
||||
}));
|
||||
|
||||
setSeveritySelectOptions(options);
|
||||
}, [actionConnector, severity]);
|
||||
|
||||
// Reset parameters when changing connector
|
||||
useEffect(() => {
|
||||
if (!firstLoad) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIncidentTypesComboBoxOptions([]);
|
||||
setSelectedIncidentTypesComboBoxOptions([]);
|
||||
setSeveritySelectOptions([]);
|
||||
editAction('subActionParams', { title, comments, description: '', savedObjectId }, index);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [actionConnector]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!actionParams.subAction) {
|
||||
editAction('subAction', 'pushToService', index);
|
||||
}
|
||||
if (!savedObjectId && messageVariables?.find((variable) => variable.name === 'alertId')) {
|
||||
editSubActionProperty('savedObjectId', '{{alertId}}');
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [actionConnector, savedObjectId]);
|
||||
|
||||
useEffect(() => {
|
||||
setIncidentTypesComboBoxOptions(
|
||||
allIncidentTypes
|
||||
? allIncidentTypes.map((type: { id: number; name: string }) => ({
|
||||
label: type.name,
|
||||
value: type.id.toString(),
|
||||
}))
|
||||
: []
|
||||
);
|
||||
|
||||
const allIncidentTypesAsObject = allIncidentTypes.reduce(
|
||||
(acc, type) => ({ ...acc, [type.id.toString()]: type.name }),
|
||||
{} as Record<string, string>
|
||||
);
|
||||
|
||||
setSelectedIncidentTypesComboBoxOptions(
|
||||
incidentTypes
|
||||
? incidentTypes
|
||||
.map((type) => ({
|
||||
label: allIncidentTypesAsObject[type.toString()],
|
||||
value: type.toString(),
|
||||
}))
|
||||
.filter((type) => type.label != null)
|
||||
: []
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [actionConnector, allIncidentTypes]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiTitle size="s">
|
||||
<h3>Incident</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.urgencySelectFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Incident Type',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiComboBox
|
||||
fullWidth
|
||||
isLoading={isLoadingIncidentTypes}
|
||||
isDisabled={isLoadingIncidentTypes}
|
||||
data-test-subj="incidentTypeComboBox"
|
||||
options={incidentTypesComboBoxOptions}
|
||||
selectedOptions={selectedIncidentTypesComboBoxOptions}
|
||||
onChange={(selectedOptions: Array<{ label: string; value?: string }>) => {
|
||||
setSelectedIncidentTypesComboBoxOptions(
|
||||
selectedOptions.map((selectedOption) => ({
|
||||
label: selectedOption.label,
|
||||
value: selectedOption.value,
|
||||
}))
|
||||
);
|
||||
|
||||
editSubActionProperty(
|
||||
'incidentTypes',
|
||||
selectedOptions.map((selectedOption) => selectedOption.value ?? selectedOption.label)
|
||||
);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!incidentTypes) {
|
||||
editSubActionProperty('incidentTypes', []);
|
||||
}
|
||||
}}
|
||||
isClearable={true}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.severity',
|
||||
{
|
||||
defaultMessage: 'Severity',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiSelect
|
||||
isLoading={isLoadingSeverity}
|
||||
disabled={isLoadingSeverity}
|
||||
fullWidth
|
||||
data-test-subj="severitySelect"
|
||||
options={severitySelectOptions}
|
||||
value={severityCode}
|
||||
onChange={(e) => {
|
||||
editSubActionProperty('severityCode', e.target.value);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
error={errors.title}
|
||||
isInvalid={errors.title.length > 0 && title !== undefined}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.titleFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Name',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<TextFieldWithMessageVariables
|
||||
index={index}
|
||||
editAction={editSubActionProperty}
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'title'}
|
||||
inputTargetValue={title}
|
||||
errors={errors.title as string[]}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<TextAreaWithMessageVariables
|
||||
index={index}
|
||||
editAction={editSubActionProperty}
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'description'}
|
||||
inputTargetValue={description}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.descriptionTextAreaFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Description (optional)',
|
||||
}
|
||||
)}
|
||||
errors={errors.description as string[]}
|
||||
/>
|
||||
<TextAreaWithMessageVariables
|
||||
index={index}
|
||||
editAction={(key, value) => {
|
||||
editSubActionProperty(key, [{ commentId: 'alert-comment', comment: value }]);
|
||||
}}
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'comments'}
|
||||
inputTargetValue={comments && comments.length > 0 ? comments[0].comment : ''}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.commentsTextAreaFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Additional comments (optional)',
|
||||
}
|
||||
)}
|
||||
errors={errors.comments as string[]}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { ResilientParamsFields as default };
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const DESC = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.selectMessageText',
|
||||
{
|
||||
defaultMessage: 'Push or update data to a new incident in Resilient.',
|
||||
}
|
||||
);
|
||||
|
||||
export const TITLE = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.actionTypeTitle',
|
||||
{
|
||||
defaultMessage: 'Resilient',
|
||||
}
|
||||
);
|
||||
|
||||
export const API_URL_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiUrlTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'URL',
|
||||
}
|
||||
);
|
||||
|
||||
export const API_URL_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiUrlTextField',
|
||||
{
|
||||
defaultMessage: 'URL is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const API_URL_INVALID = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.invalidApiUrlTextField',
|
||||
{
|
||||
defaultMessage: 'URL is invalid.',
|
||||
}
|
||||
);
|
||||
|
||||
export const ORG_ID_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.orgId',
|
||||
{
|
||||
defaultMessage: 'Organization ID',
|
||||
}
|
||||
);
|
||||
|
||||
export const ORG_ID_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredOrgIdTextField',
|
||||
{
|
||||
defaultMessage: 'Organization ID is required',
|
||||
}
|
||||
);
|
||||
|
||||
export const API_KEY_ID_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeyId',
|
||||
{
|
||||
defaultMessage: 'API key ID',
|
||||
}
|
||||
);
|
||||
|
||||
export const API_KEY_ID_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeyIdTextField',
|
||||
{
|
||||
defaultMessage: 'API key ID is required',
|
||||
}
|
||||
);
|
||||
|
||||
export const API_KEY_SECRET_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeySecret',
|
||||
{
|
||||
defaultMessage: 'API key secret',
|
||||
}
|
||||
);
|
||||
|
||||
export const API_KEY_SECRET_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeySecretTextField',
|
||||
{
|
||||
defaultMessage: 'API key secret is required',
|
||||
}
|
||||
);
|
||||
|
||||
export const MAPPING_FIELD_NAME = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldShortDescription',
|
||||
{
|
||||
defaultMessage: 'Name',
|
||||
}
|
||||
);
|
||||
|
||||
export const MAPPING_FIELD_DESC = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldDescription',
|
||||
{
|
||||
defaultMessage: 'Description',
|
||||
}
|
||||
);
|
||||
|
||||
export const MAPPING_FIELD_COMMENTS = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.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.requiredTitleTextField',
|
||||
{
|
||||
defaultMessage: 'Title is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const INCIDENT_TYPES_API_ERROR = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.unableToGetIncidentTypesMessage',
|
||||
{
|
||||
defaultMessage: 'Unable to get incident types',
|
||||
}
|
||||
);
|
||||
|
||||
export const SEVERITY_API_ERROR = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.unableToGetSeverityMessage',
|
||||
{
|
||||
defaultMessage: 'Unable to get severity',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { CasesConfigurationMapping } from '../case_mappings';
|
||||
|
||||
export interface ResilientActionConnector {
|
||||
config: ResilientConfig;
|
||||
secrets: ResilientSecrets;
|
||||
}
|
||||
|
||||
export interface ResilientActionParams {
|
||||
subAction: string;
|
||||
subActionParams: {
|
||||
savedObjectId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
externalId: string | null;
|
||||
incidentTypes: number[];
|
||||
severityCode: number;
|
||||
comments: Array<{ commentId: string; comment: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
interface IncidentConfiguration {
|
||||
mapping: CasesConfigurationMapping[];
|
||||
}
|
||||
|
||||
interface ResilientConfig {
|
||||
apiUrl: string;
|
||||
orgId: string;
|
||||
incidentConfiguration?: IncidentConfiguration;
|
||||
isCaseOwned?: boolean;
|
||||
}
|
||||
|
||||
interface ResilientSecrets {
|
||||
apiKeyId: string;
|
||||
apiKeySecret: string;
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
import { HttpSetup, ToastsApi } from 'kibana/public';
|
||||
import { ActionConnector } from '../../../../types';
|
||||
import { getIncidentTypes } from './api';
|
||||
import * as i18n from './translations';
|
||||
|
||||
type IncidentTypes = Array<{ id: number; name: string }>;
|
||||
|
||||
interface Props {
|
||||
http: HttpSetup;
|
||||
toastNotifications: Pick<
|
||||
ToastsApi,
|
||||
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
|
||||
>;
|
||||
actionConnector?: ActionConnector;
|
||||
}
|
||||
|
||||
export interface UseGetIncidentTypes {
|
||||
incidentTypes: IncidentTypes;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const useGetIncidentTypes = ({
|
||||
http,
|
||||
toastNotifications,
|
||||
actionConnector,
|
||||
}: Props): UseGetIncidentTypes => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [incidentTypes, setIncidentTypes] = useState<IncidentTypes>([]);
|
||||
const abortCtrl = useRef(new AbortController());
|
||||
|
||||
useEffect(() => {
|
||||
let didCancel = false;
|
||||
const fetchData = async () => {
|
||||
if (!actionConnector) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
abortCtrl.current = new AbortController();
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const res = await getIncidentTypes({
|
||||
http,
|
||||
signal: abortCtrl.current.signal,
|
||||
connectorId: actionConnector.id,
|
||||
});
|
||||
|
||||
if (!didCancel) {
|
||||
setIsLoading(false);
|
||||
setIncidentTypes(res.data ?? []);
|
||||
if (res.status && res.status === 'error') {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.INCIDENT_TYPES_API_ERROR,
|
||||
text: `${res.serviceMessage ?? res.message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (!didCancel) {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.INCIDENT_TYPES_API_ERROR,
|
||||
text: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
abortCtrl.current.abort();
|
||||
fetchData();
|
||||
|
||||
return () => {
|
||||
didCancel = true;
|
||||
setIsLoading(false);
|
||||
abortCtrl.current.abort();
|
||||
};
|
||||
}, [http, actionConnector, toastNotifications]);
|
||||
|
||||
return {
|
||||
incidentTypes,
|
||||
isLoading,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
import { HttpSetup, ToastsApi } from 'kibana/public';
|
||||
import { ActionConnector } from '../../../../types';
|
||||
import { getSeverity } from './api';
|
||||
import * as i18n from './translations';
|
||||
|
||||
type Severity = Array<{ id: number; name: string }>;
|
||||
|
||||
interface Props {
|
||||
http: HttpSetup;
|
||||
toastNotifications: Pick<
|
||||
ToastsApi,
|
||||
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
|
||||
>;
|
||||
actionConnector?: ActionConnector;
|
||||
}
|
||||
|
||||
export interface UseGetSeverity {
|
||||
severity: Severity;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const useGetSeverity = ({
|
||||
http,
|
||||
toastNotifications,
|
||||
actionConnector,
|
||||
}: Props): UseGetSeverity => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [severity, setSeverity] = useState<Severity>([]);
|
||||
const abortCtrl = useRef(new AbortController());
|
||||
|
||||
useEffect(() => {
|
||||
let didCancel = false;
|
||||
const fetchData = async () => {
|
||||
if (!actionConnector) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
abortCtrl.current = new AbortController();
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const res = await getSeverity({
|
||||
http,
|
||||
signal: abortCtrl.current.signal,
|
||||
connectorId: actionConnector.id,
|
||||
});
|
||||
|
||||
if (!didCancel) {
|
||||
setIsLoading(false);
|
||||
setSeverity(res.data ?? []);
|
||||
|
||||
if (res.status && res.status === 'error') {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.SEVERITY_API_ERROR,
|
||||
text: `${res.serviceMessage ?? res.message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (!didCancel) {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.SEVERITY_API_ERROR,
|
||||
text: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
abortCtrl.current.abort();
|
||||
fetchData();
|
||||
|
||||
return () => {
|
||||
didCancel = true;
|
||||
setIsLoading(false);
|
||||
abortCtrl.current.abort();
|
||||
};
|
||||
}, [http, actionConnector, toastNotifications]);
|
||||
|
||||
return {
|
||||
severity,
|
||||
isLoading,
|
||||
};
|
||||
};
|
|
@ -23,6 +23,7 @@ describe('ServiceNowParamsFields renders', () => {
|
|||
externalId: null,
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<ServiceNowParamsFields
|
||||
actionParams={actionParams}
|
||||
|
|
|
@ -13,6 +13,7 @@ describe('SlackParamsFields renders', () => {
|
|||
const actionParams = {
|
||||
message: 'test message',
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<SlackParamsFields
|
||||
actionParams={actionParams}
|
||||
|
|
|
@ -13,6 +13,7 @@ describe('WebhookParamsFields renders', () => {
|
|||
const actionParams = {
|
||||
body: 'test message',
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<WebhookParamsFields
|
||||
actionParams={actionParams}
|
||||
|
|
|
@ -8,3 +8,4 @@ export * from './expression_items';
|
|||
|
||||
export { connectorConfiguration as ServiceNowConnectorConfiguration } 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';
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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 { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
import {
|
||||
getExternalServiceSimulatorPath,
|
||||
ExternalServiceSimulator,
|
||||
} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin';
|
||||
|
||||
const mapping = [
|
||||
{
|
||||
source: 'title',
|
||||
target: 'description',
|
||||
actionType: 'nothing',
|
||||
},
|
||||
{
|
||||
source: 'description',
|
||||
target: 'short_description',
|
||||
actionType: 'nothing',
|
||||
},
|
||||
{
|
||||
source: 'comments',
|
||||
target: 'comments',
|
||||
actionType: 'nothing',
|
||||
},
|
||||
];
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function resilientTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const mockResilient = {
|
||||
config: {
|
||||
apiUrl: 'www.resilientisinkibanaactions.com',
|
||||
orgId: '201',
|
||||
incidentConfiguration: { mapping: [...mapping] },
|
||||
isCaseOwned: true,
|
||||
},
|
||||
secrets: {
|
||||
apiKeyId: 'elastic',
|
||||
apiKeySecret: 'changeme',
|
||||
},
|
||||
params: {
|
||||
savedObjectId: '123',
|
||||
title: 'a title',
|
||||
description: 'a description',
|
||||
incidentTypes: [1001],
|
||||
severityCode: 'High',
|
||||
comments: [
|
||||
{
|
||||
commentId: '456',
|
||||
comment: 'first comment',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
describe('resilient', () => {
|
||||
let resilientSimulatorURL: string = '<could not determine kibana url>';
|
||||
|
||||
// need to wait for kibanaServer to settle ...
|
||||
before(() => {
|
||||
resilientSimulatorURL = kibanaServer.resolveUrl(
|
||||
getExternalServiceSimulatorPath(ExternalServiceSimulator.RESILIENT)
|
||||
);
|
||||
});
|
||||
|
||||
it('should return 403 when creating a resilient action', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/action')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A resilient action',
|
||||
actionTypeId: '.resilient',
|
||||
config: {
|
||||
apiUrl: resilientSimulatorURL,
|
||||
incidentConfiguration: { ...mockResilient.config.incidentConfiguration },
|
||||
isCaseOwned: true,
|
||||
},
|
||||
secrets: mockResilient.secrets,
|
||||
})
|
||||
.expect(403, {
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message:
|
||||
'Action type .resilient is disabled because your basic license does not support it. Please upgrade your license.',
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -41,9 +41,10 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
|
||||
const mockResilient = {
|
||||
config: {
|
||||
apiUrl: 'www.jiraisinkibanaactions.com',
|
||||
apiUrl: 'www.resilientisinkibanaactions.com',
|
||||
orgId: '201',
|
||||
casesConfiguration: { mapping },
|
||||
incidentConfiguration: { mapping },
|
||||
isCaseOwned: true,
|
||||
},
|
||||
secrets: {
|
||||
apiKeyId: 'key',
|
||||
|
@ -55,6 +56,8 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
savedObjectId: '123',
|
||||
title: 'a title',
|
||||
description: 'a description',
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
createdAt: '2020-03-13T08:34:53.450Z',
|
||||
createdBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
updatedAt: null,
|
||||
|
@ -108,7 +111,8 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
config: {
|
||||
apiUrl: resilientSimulatorURL,
|
||||
orgId: mockResilient.config.orgId,
|
||||
casesConfiguration: mockResilient.config.casesConfiguration,
|
||||
incidentConfiguration: mockResilient.config.incidentConfiguration,
|
||||
isCaseOwned: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -124,7 +128,8 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
config: {
|
||||
apiUrl: resilientSimulatorURL,
|
||||
orgId: mockResilient.config.orgId,
|
||||
casesConfiguration: mockResilient.config.casesConfiguration,
|
||||
incidentConfiguration: mockResilient.config.incidentConfiguration,
|
||||
isCaseOwned: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -179,7 +184,7 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
config: {
|
||||
apiUrl: 'http://resilient.mynonexistent.com',
|
||||
orgId: mockResilient.config.orgId,
|
||||
casesConfiguration: mockResilient.config.casesConfiguration,
|
||||
incidentConfiguration: mockResilient.config.incidentConfiguration,
|
||||
},
|
||||
secrets: mockResilient.secrets,
|
||||
})
|
||||
|
@ -204,7 +209,7 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
config: {
|
||||
apiUrl: resilientSimulatorURL,
|
||||
orgId: mockResilient.config.orgId,
|
||||
casesConfiguration: mockResilient.config.casesConfiguration,
|
||||
incidentConfiguration: mockResilient.config.incidentConfiguration,
|
||||
},
|
||||
})
|
||||
.expect(400)
|
||||
|
@ -218,30 +223,6 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a ibm resilient action without casesConfiguration', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/action')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'An IBM Resilient',
|
||||
actionTypeId: '.resilient',
|
||||
config: {
|
||||
apiUrl: resilientSimulatorURL,
|
||||
orgId: mockResilient.config.orgId,
|
||||
},
|
||||
secrets: mockResilient.secrets,
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type config: [casesConfiguration.mapping]: expected value of type [array] but got [undefined]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a ibm resilient action with empty mapping', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/action')
|
||||
|
@ -252,7 +233,7 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
config: {
|
||||
apiUrl: resilientSimulatorURL,
|
||||
orgId: mockResilient.config.orgId,
|
||||
casesConfiguration: { mapping: [] },
|
||||
incidentConfiguration: { mapping: [] },
|
||||
},
|
||||
secrets: mockResilient.secrets,
|
||||
})
|
||||
|
@ -262,7 +243,7 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type config: [casesConfiguration.mapping]: expected non-empty but got empty',
|
||||
'error validating action type config: [incidentConfiguration.mapping]: expected non-empty but got empty',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -277,7 +258,7 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
config: {
|
||||
apiUrl: resilientSimulatorURL,
|
||||
orgId: mockResilient.config.orgId,
|
||||
casesConfiguration: {
|
||||
incidentConfiguration: {
|
||||
mapping: [
|
||||
{
|
||||
source: 'title',
|
||||
|
@ -307,7 +288,7 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
config: {
|
||||
apiUrl: resilientSimulatorURL,
|
||||
orgId: mockResilient.config.orgId,
|
||||
casesConfiguration: mockResilient.config.casesConfiguration,
|
||||
incidentConfiguration: mockResilient.config.incidentConfiguration,
|
||||
},
|
||||
secrets: mockResilient.secrets,
|
||||
});
|
||||
|
@ -353,7 +334,7 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
status: 'error',
|
||||
retry: false,
|
||||
message:
|
||||
'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subAction]: expected value to equal [pushToService]',
|
||||
'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subAction]: expected value to equal [pushToService]\n- [3.subAction]: expected value to equal [incidentTypes]\n- [4.subAction]: expected value to equal [severity]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -371,7 +352,7 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
status: 'error',
|
||||
retry: false,
|
||||
message:
|
||||
'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]',
|
||||
'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]\n- [3.subAction]: expected value to equal [incidentTypes]\n- [4.subAction]: expected value to equal [severity]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -389,7 +370,7 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
status: 'error',
|
||||
retry: false,
|
||||
message:
|
||||
'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]',
|
||||
'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]\n- [3.subAction]: expected value to equal [incidentTypes]\n- [4.subAction]: expected value to equal [severity]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -412,31 +393,7 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
status: 'error',
|
||||
retry: false,
|
||||
message:
|
||||
'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.title]: expected value of type [string] but got [undefined]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle failing with a simulated success without createdAt', async () => {
|
||||
await supertest
|
||||
.post(`/api/actions/action/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
...mockResilient.params,
|
||||
subActionParams: {
|
||||
savedObjectId: 'success',
|
||||
title: 'success',
|
||||
},
|
||||
},
|
||||
})
|
||||
.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 [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.createdAt]: expected value of type [string] but got [undefined]',
|
||||
'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.title]: expected value of type [string] but got [undefined]\n- [3.subAction]: expected value to equal [incidentTypes]\n- [4.subAction]: expected value to equal [severity]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -464,7 +421,7 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
status: 'error',
|
||||
retry: false,
|
||||
message:
|
||||
'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.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 [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.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- [3.subAction]: expected value to equal [incidentTypes]\n- [4.subAction]: expected value to equal [severity]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -492,35 +449,7 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
status: 'error',
|
||||
retry: false,
|
||||
message:
|
||||
'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.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]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle failing with a simulated success without comment.createdAt', async () => {
|
||||
await supertest
|
||||
.post(`/api/actions/action/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
...mockResilient.params,
|
||||
subActionParams: {
|
||||
...mockResilient.params.subActionParams,
|
||||
savedObjectId: 'success',
|
||||
title: 'success',
|
||||
createdAt: 'success',
|
||||
createdBy: { username: 'elastic' },
|
||||
comments: [{ commentId: 'success', comment: 'success' }],
|
||||
},
|
||||
},
|
||||
})
|
||||
.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 [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.createdAt]: 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 [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.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- [3.subAction]: expected value to equal [incidentTypes]\n- [4.subAction]: expected value to equal [severity]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -536,7 +465,7 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
...mockResilient.params,
|
||||
subActionParams: {
|
||||
...mockResilient.params.subActionParams,
|
||||
comments: [],
|
||||
comments: null,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
@ -99,7 +99,7 @@ export const getResilientConnector = () => ({
|
|||
config: {
|
||||
apiUrl: 'http://some.non.existent.com',
|
||||
orgId: 'pkey',
|
||||
casesConfiguration: {
|
||||
incidentConfiguration: {
|
||||
mapping: [
|
||||
{
|
||||
source: 'title',
|
||||
|
@ -118,6 +118,7 @@ export const getResilientConnector = () => ({
|
|||
},
|
||||
],
|
||||
},
|
||||
isCaseOwned: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in a new issue