;
export interface ExternalServiceCommentResponse {
commentId: string;
@@ -29,18 +27,6 @@ export interface ExternalServiceCommentResponse {
externalCommentId?: string;
}
-export type ExecutorSubActionGetIncidentParams = TypeOf<
- typeof ExecutorSubActionGetIncidentParamsSchema
->;
-
-export type ExecutorSubActionHandshakeParams = TypeOf<
- typeof ExecutorSubActionHandshakeParamsSchema
->;
-
-export interface PushToServiceResponse extends ExternalServiceIncidentResponse {
- comments?: ExternalServiceCommentResponse[];
-}
-
export interface PipedField {
key: string;
value: string;
@@ -48,10 +34,10 @@ export interface PipedField {
pipes: string[];
}
-export interface TransformFieldsArgs {
- params: PushToServiceApiParams;
+export interface TransformFieldsArgs {
+ params: P;
fields: PipedField[];
- currentIncident?: ExternalServiceParams;
+ currentIncident?: S;
}
export interface TransformerArgs {
diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts
index d895bf386a367..701bbea14fde8 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts
@@ -51,10 +51,7 @@ export const buildMap = (mapping: MapRecord[]): Map => {
}, new Map());
};
-export const mapParams = (
- params: Partial,
- mapping: Map
-): AnyParams => {
+export const mapParams = (params: T, mapping: Map): AnyParams => {
return Object.keys(params).reduce((prev: AnyParams, curr: string): AnyParams => {
const field = mapping.get(curr);
if (field) {
@@ -106,7 +103,10 @@ export const createConnectorExecutor = ({
const { comments, externalId, ...restParams } = pushToServiceParams;
const mapping = buildMap(config.casesConfiguration.mapping);
- const externalCase = mapParams(restParams, mapping);
+ const externalCase = mapParams(
+ restParams as ExecutorSubActionPushParams,
+ mapping
+ );
data = await api.pushToService({
externalService,
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts
index bcfb82077d286..4495c37f758ee 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts
@@ -4,15 +4,18 @@
* 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 { externalServiceMock, mapping, apiParams } from './mocks';
-import { ExternalService } from '../case/types';
+import { ExternalService } from './types';
+import { api } from './api';
+let mockedLogger: jest.Mocked;
describe('api', () => {
let externalService: jest.Mocked;
beforeEach(() => {
externalService = externalServiceMock.create();
+ jest.clearAllMocks();
});
afterEach(() => {
@@ -20,10 +23,15 @@ describe('api', () => {
});
describe('pushToService', () => {
- describe('create incident', () => {
+ describe('create incident - cases', () => {
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: 'incident-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: 'incident-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: {
@@ -69,9 +82,25 @@ describe('api', () => {
expect(externalService.updateIncident).not.toHaveBeenCalled();
});
+ test('it calls createIncident correctly without mapping', async () => {
+ const params = { ...apiParams, externalId: null };
+ await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger });
+
+ expect(externalService.createIncident).toHaveBeenCalledWith({
+ incident: {
+ description: 'Incident description',
+ summary: 'Incident title',
+ issueType: '10006',
+ labels: ['kibana', 'elastic'],
+ priority: 'High',
+ },
+ });
+ expect(externalService.updateIncident).not.toHaveBeenCalled();
+ });
+
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: 'incident-1',
@@ -89,7 +118,6 @@ describe('api', () => {
username: 'elastic',
},
},
- field: 'comments',
});
expect(externalService.createComment).toHaveBeenNthCalledWith(2, {
@@ -108,14 +136,59 @@ describe('api', () => {
username: 'elastic',
},
},
- field: 'comments',
+ });
+ });
+
+ test('it calls createComment correctly without mapping', async () => {
+ const params = { ...apiParams, externalId: null };
+ await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger });
+ expect(externalService.createComment).toHaveBeenCalledTimes(2);
+ expect(externalService.createComment).toHaveBeenNthCalledWith(1, {
+ incidentId: 'incident-1',
+ comment: {
+ commentId: 'case-comment-1',
+ comment: 'A comment',
+ createdAt: '2020-04-27T10:59:46.202Z',
+ createdBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ updatedAt: '2020-04-27T10:59:46.202Z',
+ updatedBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ },
+ });
+
+ expect(externalService.createComment).toHaveBeenNthCalledWith(2, {
+ incidentId: 'incident-1',
+ comment: {
+ commentId: 'case-comment-2',
+ comment: 'Another comment',
+ createdAt: '2020-04-27T10:59:46.202Z',
+ createdBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ updatedAt: '2020-04-27T10:59:46.202Z',
+ updatedBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ },
});
});
});
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: 'incident-1',
@@ -137,7 +210,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: 'incident-1',
@@ -149,7 +227,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',
@@ -162,9 +240,26 @@ describe('api', () => {
expect(externalService.createIncident).not.toHaveBeenCalled();
});
+ test('it calls updateIncident correctly without mapping', async () => {
+ const params = { ...apiParams };
+ await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger });
+
+ expect(externalService.updateIncident).toHaveBeenCalledWith({
+ incidentId: 'incident-3',
+ incident: {
+ description: 'Incident description',
+ summary: 'Incident title',
+ issueType: '10006',
+ labels: ['kibana', 'elastic'],
+ priority: 'High',
+ },
+ });
+ expect(externalService.createIncident).not.toHaveBeenCalled();
+ });
+
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: 'incident-1',
@@ -182,7 +277,6 @@ describe('api', () => {
username: 'elastic',
},
},
- field: 'comments',
});
expect(externalService.createComment).toHaveBeenNthCalledWith(2, {
@@ -201,7 +295,87 @@ describe('api', () => {
username: 'elastic',
},
},
- field: 'comments',
+ });
+ });
+
+ test('it calls createComment correctly without mapping', async () => {
+ const params = { ...apiParams };
+ await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger });
+ expect(externalService.createComment).toHaveBeenCalledTimes(2);
+ expect(externalService.createComment).toHaveBeenNthCalledWith(1, {
+ incidentId: 'incident-1',
+ comment: {
+ commentId: 'case-comment-1',
+ comment: 'A comment',
+ createdAt: '2020-04-27T10:59:46.202Z',
+ createdBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ updatedAt: '2020-04-27T10:59:46.202Z',
+ updatedBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ },
+ });
+
+ expect(externalService.createComment).toHaveBeenNthCalledWith(2, {
+ incidentId: 'incident-1',
+ comment: {
+ commentId: 'case-comment-2',
+ comment: 'Another comment',
+ createdAt: '2020-04-27T10:59:46.202Z',
+ createdBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ updatedAt: '2020-04-27T10:59:46.202Z',
+ updatedBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ },
+ });
+ });
+ });
+
+ describe('issueTypes', () => {
+ test('it returns the issue types correctly', async () => {
+ const res = await api.issueTypes({
+ externalService,
+ params: {},
+ });
+ expect(res).toEqual([
+ {
+ id: '10006',
+ name: 'Task',
+ },
+ {
+ id: '10007',
+ name: 'Bug',
+ },
+ ]);
+ });
+ });
+
+ describe('fieldsByIssueType', () => {
+ test('it returns the fields correctly', async () => {
+ const res = await api.fieldsByIssueType({
+ externalService,
+ params: { id: '10006' },
+ });
+ expect(res).toEqual({
+ summary: { allowedValues: [], defaultValue: {} },
+ priority: {
+ allowedValues: [
+ {
+ name: 'Medium',
+ id: '3',
+ },
+ ],
+ defaultValue: { name: 'Medium', id: '3' },
+ },
});
});
});
@@ -228,7 +402,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 +439,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 +475,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 +513,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 +546,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 +581,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 +618,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 +654,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 +692,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 +728,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();
});
});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts
index 3db66e5884af4..da47a4bfb839b 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts
@@ -4,4 +4,179 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { api } from '../case/api';
+import { flow } from 'lodash';
+import {
+ ExternalServiceParams,
+ PushToServiceApiHandlerArgs,
+ HandshakeApiHandlerArgs,
+ GetIncidentApiHandlerArgs,
+ ExternalServiceApi,
+ Incident,
+ GetFieldsByIssueTypeHandlerArgs,
+ GetIssueTypesHandlerArgs,
+ PushToServiceApiParams,
+} 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';
+
+const handshakeHandler = async ({
+ externalService,
+ mapping,
+ params,
+}: HandshakeApiHandlerArgs) => {};
+
+const getIncidentHandler = async ({
+ externalService,
+ mapping,
+ params,
+}: GetIncidentApiHandlerArgs) => {};
+
+const getIssueTypesHandler = async ({ externalService }: GetIssueTypesHandlerArgs) => {
+ const res = await externalService.getIssueTypes();
+ return res;
+};
+
+const getFieldsByIssueTypeHandler = async ({
+ externalService,
+ params,
+}: GetFieldsByIssueTypeHandlerArgs) => {
+ const { id } = params;
+ const res = await externalService.getFieldsByIssueType(id);
+ return res;
+};
+
+const pushToServiceHandler = async ({
+ externalService,
+ mapping,
+ params,
+ logger,
+}: PushToServiceApiHandlerArgs): Promise => {
+ 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 Jira 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({
+ params,
+ fields,
+ currentIncident,
+ });
+ } else {
+ const { title, description, priority, labels, issueType } = params;
+ incident = { summary: title, description, priority, labels, issueType };
+ }
+
+ 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 transformFields = ({
+ params,
+ fields,
+ currentIncident,
+}: TransformFieldsArgs): 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,
+ getIncident: getIncidentHandler,
+ issueTypes: getIssueTypesHandler,
+ fieldsByIssueType: getFieldsByIssueTypeHandler,
+};
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts
index 66be0bad02d7b..d3346557f3684 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts
@@ -4,33 +4,138 @@
* 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 { ActionType } from '../../types';
+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 { JiraSecretConfiguration, JiraPublicConfiguration } from './schema';
+import {
+ ExternalIncidentServiceConfiguration,
+ ExternalIncidentServiceSecretConfiguration,
+ ExecutorParamsSchema,
+} from './schema';
import { ActionsConfigurationUtilities } from '../../actions_config';
+import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../../types';
+import { createExternalService } from './service';
+import { api } from './api';
+import {
+ ExecutorParams,
+ ExecutorSubActionPushParams,
+ JiraPublicConfigurationType,
+ JiraSecretConfigurationType,
+ JiraExecutorResultData,
+ ExecutorSubActionGetFieldsByIssueTypeParams,
+ ExecutorSubActionGetIssueTypesParams,
+} 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: JiraPublicConfiguration,
- secrets: JiraSecretConfiguration,
+}
+
+const supportedSubActions: string[] = ['pushToService', 'issueTypes', 'fieldsByIssueType'];
+
+// action type definition
+export function getActionType(
+ params: GetActionTypeParams
+): ActionType<
+ JiraPublicConfigurationType,
+ JiraSecretConfigurationType,
+ ExecutorParams,
+ JiraExecutorResultData | {}
+> {
+ const { logger, configurationUtilities } = params;
+ return {
+ id: '.jira',
+ minimumLicenseRequired: 'gold',
+ 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<
+ JiraPublicConfigurationType,
+ JiraSecretConfigurationType,
+ ExecutorParams
+ >
+): Promise> {
+ const { actionId, config, params, secrets } = execOptions;
+ const { subAction, subActionParams } = params as ExecutorParams;
+ let data: JiraExecutorResultData | 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 incidentConfiguration = config.incidentConfiguration;
+ const mapping = incidentConfiguration ? buildMap(incidentConfiguration.mapping) : null;
+ const externalObject =
+ config.incidentConfiguration && mapping
+ ? mapParams(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 === 'issueTypes') {
+ const getIssueTypesParams = subActionParams as ExecutorSubActionGetIssueTypesParams;
+ data = await api.issueTypes({
+ externalService,
+ params: getIssueTypesParams,
+ });
+ }
+
+ if (subAction === 'fieldsByIssueType') {
+ const getFieldsByIssueTypeParams = subActionParams as ExecutorSubActionGetFieldsByIssueTypeParams;
+ data = await api.fieldsByIssueType({
+ externalService,
+ params: getFieldsByIssueTypeParams,
+ });
+ }
+
+ return { status: 'ok', data: data ?? {}, actionId };
}
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts
index 709d490a5227f..e7841996fedef 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts
@@ -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/common_types';
const createMock = (): jest.Mocked => {
const service = {
@@ -40,6 +37,30 @@ const createMock = (): jest.Mocked => {
})
),
createComment: jest.fn(),
+ findIncidents: jest.fn(),
+ getCapabilities: jest.fn(),
+ getIssueTypes: jest.fn().mockImplementation(() => [
+ {
+ id: '10006',
+ name: 'Task',
+ },
+ {
+ id: '10007',
+ name: 'Bug',
+ },
+ ]),
+ getFieldsByIssueType: jest.fn().mockImplementation(() => ({
+ summary: { allowedValues: [], defaultValue: {} },
+ priority: {
+ allowedValues: [
+ {
+ name: 'Medium',
+ id: '3',
+ },
+ ],
+ defaultValue: { name: 'Medium', id: '3' },
+ },
+ })),
};
service.createComment.mockImplementationOnce(() =>
@@ -96,6 +117,9 @@ const executorParams: ExecutorSubActionPushParams = {
updatedBy: { fullName: 'Elastic User', username: 'elastic' },
title: 'Incident title',
description: 'Incident description',
+ labels: ['kibana', 'elastic'],
+ priority: 'High',
+ issueType: '10006',
comments: [
{
commentId: 'case-comment-1',
@@ -118,7 +142,7 @@ const executorParams: ExecutorSubActionPushParams = {
const apiParams: PushToServiceApiParams = {
...executorParams,
- externalCase: { summary: 'Incident title', description: 'Incident description' },
+ externalObject: { summary: 'Incident title', description: 'Incident description' },
};
export { externalServiceMock, mapping, executorParams, apiParams };
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts
index 9c831e75d91c1..07c8e22812b27 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts
@@ -5,18 +5,85 @@
*/
import { schema } from '@kbn/config-schema';
-import { ExternalIncidentServiceConfiguration } from '../case/schema';
+import {
+ CommentSchema,
+ EntityInformation,
+ IncidentConfigurationSchema,
+} from '../case/common_schema';
-export const JiraPublicConfiguration = {
+export const ExternalIncidentServiceConfiguration = {
+ apiUrl: schema.string(),
projectKey: schema.string(),
- ...ExternalIncidentServiceConfiguration,
+ // TODO: to remove - set it optional for the current stage to support Case Jira implementation
+ incidentConfiguration: schema.nullable(IncidentConfigurationSchema),
+ isCaseOwned: schema.nullable(schema.boolean()),
};
-export const JiraPublicConfigurationSchema = schema.object(JiraPublicConfiguration);
+export const ExternalIncidentServiceConfigurationSchema = schema.object(
+ ExternalIncidentServiceConfiguration
+);
-export const JiraSecretConfiguration = {
+export const ExternalIncidentServiceSecretConfiguration = {
email: schema.string(),
apiToken: schema.string(),
};
-export const JiraSecretConfigurationSchema = schema.object(JiraSecretConfiguration);
+export const ExternalIncidentServiceSecretConfigurationSchema = schema.object(
+ ExternalIncidentServiceSecretConfiguration
+);
+
+export const ExecutorSubActionSchema = schema.oneOf([
+ schema.literal('getIncident'),
+ schema.literal('pushToService'),
+ schema.literal('handshake'),
+ schema.literal('issueTypes'),
+ schema.literal('fieldsByIssueType'),
+]);
+
+export const ExecutorSubActionPushParamsSchema = schema.object({
+ savedObjectId: schema.string(),
+ title: schema.string(),
+ description: schema.nullable(schema.string()),
+ externalId: schema.nullable(schema.string()),
+ issueType: schema.nullable(schema.string()),
+ priority: schema.nullable(schema.string()),
+ labels: schema.nullable(schema.arrayOf(schema.string())),
+ // TODO: modify later to string[] - need for support Case schema
+ 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 ExecutorSubActionGetCapabilitiesParamsSchema = schema.object({});
+export const ExecutorSubActionGetIssueTypesParamsSchema = schema.object({});
+export const ExecutorSubActionGetFieldsByIssueTypeParamsSchema = schema.object({
+ id: schema.string(),
+});
+
+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('issueTypes'),
+ subActionParams: ExecutorSubActionGetIssueTypesParamsSchema,
+ }),
+ schema.object({
+ subAction: schema.literal('fieldsByIssueType'),
+ subActionParams: ExecutorSubActionGetFieldsByIssueTypeParamsSchema,
+ }),
+]);
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts
index 547595b4c183f..2439c507c3328 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts
@@ -8,11 +8,15 @@ import axios from 'axios';
import { createExternalService } 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';
const logger = loggingSystemMock.create().get() as jest.Mocked;
+interface ResponseError extends Error {
+ response?: { data: { errors: Record } };
+}
+
jest.mock('axios');
jest.mock('../lib/axios_utils', () => {
const originalUtils = jest.requireActual('../lib/axios_utils');
@@ -25,6 +29,72 @@ jest.mock('../lib/axios_utils', () => {
axios.create = jest.fn(() => axios);
const requestMock = utils.request as jest.Mock;
+const issueTypesResponse = {
+ data: {
+ projects: [
+ {
+ issuetypes: [
+ {
+ id: '10006',
+ name: 'Task',
+ },
+ {
+ id: '10007',
+ name: 'Bug',
+ },
+ ],
+ },
+ ],
+ },
+};
+
+const fieldsResponse = {
+ data: {
+ projects: [
+ {
+ issuetypes: [
+ {
+ id: '10006',
+ name: 'Task',
+ fields: {
+ summary: { fieldId: 'summary' },
+ priority: {
+ fieldId: 'priority',
+ allowedValues: [
+ {
+ name: 'Highest',
+ id: '1',
+ },
+ {
+ name: 'High',
+ id: '2',
+ },
+ {
+ name: 'Medium',
+ id: '3',
+ },
+ {
+ name: 'Low',
+ id: '4',
+ },
+ {
+ name: 'Lowest',
+ id: '5',
+ },
+ ],
+ defaultValue: {
+ name: 'Medium',
+ id: '3',
+ },
+ },
+ },
+ },
+ ],
+ },
+ ],
+ },
+};
+
describe('Jira service', () => {
let service: ExternalService;
@@ -116,19 +186,24 @@ describe('Jira service', () => {
test('it should throw an error', async () => {
requestMock.mockImplementation(() => {
- throw new Error('An error has occurred');
+ const error: ResponseError = new Error('An error has occurred');
+ error.response = { data: { errors: { summary: 'Required field' } } };
+ throw error;
});
expect(service.getIncident('1')).rejects.toThrow(
- 'Unable to get incident with id 1. Error: An error has occurred'
+ '[Action][Jira]: Unable to get incident with id 1. Error: An error has occurred Reason: Required field'
);
});
});
describe('createIncident', () => {
test('it creates the incident correctly', async () => {
- // The response from Jira when creating an issue contains only the key and the id.
- // The service makes two calls when creating an issue. One to create and one to get
- // the created incident with all the necessary fields.
+ /* The response from Jira when creating an issue contains only the key and the id.
+ The function makes the following calls when creating an issue:
+ 1. Get issueTypes to set a default ONLY when incident.issueType is missing
+ 2. Create the issue.
+ 3. Get the created issue with all the necessary fields.
+ */
requestMock.mockImplementationOnce(() => ({
data: { id: '1', key: 'CK-1', fields: { summary: 'title', description: 'description' } },
}));
@@ -138,7 +213,13 @@ describe('Jira service', () => {
}));
const res = await service.createIncident({
- incident: { summary: 'title', description: 'desc' },
+ incident: {
+ summary: 'title',
+ description: 'desc',
+ labels: [],
+ issueType: '10006',
+ priority: 'High',
+ },
});
expect(res).toEqual({
@@ -149,6 +230,68 @@ describe('Jira service', () => {
});
});
+ test('it creates the incident correctly without issue type', async () => {
+ /* The response from Jira when creating an issue contains only the key and the id.
+ The function makes the following calls when creating an issue:
+ 1. Get issueTypes to set a default ONLY when incident.issueType is missing
+ 2. Create the issue.
+ 3. Get the created issue with all the necessary fields.
+ */
+ // getIssueType mocks
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ capabilities: {
+ navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation',
+ },
+ },
+ }));
+
+ // getIssueType mocks
+ requestMock.mockImplementationOnce(() => issueTypesResponse);
+
+ requestMock.mockImplementationOnce(() => ({
+ data: { id: '1', key: 'CK-1', fields: { summary: 'title', description: 'description' } },
+ }));
+
+ requestMock.mockImplementationOnce(() => ({
+ data: { id: '1', key: 'CK-1', fields: { created: '2020-04-27T10:59:46.202Z' } },
+ }));
+
+ const res = await service.createIncident({
+ incident: {
+ summary: 'title',
+ description: 'desc',
+ labels: [],
+ priority: 'High',
+ issueType: null,
+ },
+ });
+
+ expect(res).toEqual({
+ title: 'CK-1',
+ id: '1',
+ pushedDate: '2020-04-27T10:59:46.202Z',
+ url: 'https://siem-kibana.atlassian.net/browse/CK-1',
+ });
+
+ expect(requestMock).toHaveBeenCalledWith({
+ axios,
+ url: 'https://siem-kibana.atlassian.net/rest/api/2/issue',
+ logger,
+ method: 'post',
+ data: {
+ fields: {
+ summary: 'title',
+ description: 'desc',
+ project: { key: 'CK' },
+ issuetype: { id: '10006' },
+ labels: [],
+ priority: { name: 'High' },
+ },
+ },
+ });
+ });
+
test('it should call request with correct arguments', async () => {
requestMock.mockImplementation(() => ({
data: {
@@ -159,7 +302,13 @@ describe('Jira service', () => {
}));
await service.createIncident({
- incident: { summary: 'title', description: 'desc' },
+ incident: {
+ summary: 'title',
+ description: 'desc',
+ labels: [],
+ issueType: '10006',
+ priority: 'High',
+ },
});
expect(requestMock).toHaveBeenCalledWith({
@@ -172,7 +321,9 @@ describe('Jira service', () => {
summary: 'title',
description: 'desc',
project: { key: 'CK' },
- issuetype: { name: 'Task' },
+ issuetype: { id: '10006' },
+ labels: [],
+ priority: { name: 'High' },
},
},
});
@@ -180,14 +331,24 @@ describe('Jira service', () => {
test('it should throw an error', async () => {
requestMock.mockImplementation(() => {
- throw new Error('An error has occurred');
+ const error: ResponseError = new Error('An error has occurred');
+ error.response = { data: { errors: { summary: 'Required field' } } };
+ throw error;
});
expect(
service.createIncident({
- incident: { summary: 'title', description: 'desc' },
+ incident: {
+ summary: 'title',
+ description: 'desc',
+ labels: [],
+ issueType: '10006',
+ priority: 'High',
+ },
})
- ).rejects.toThrow('[Action][Jira]: Unable to create incident. Error: An error has occurred');
+ ).rejects.toThrow(
+ '[Action][Jira]: Unable to create incident. Error: An error has occurred. Reason: Required field'
+ );
});
});
@@ -203,7 +364,13 @@ describe('Jira service', () => {
const res = await service.updateIncident({
incidentId: '1',
- incident: { summary: 'title', description: 'desc' },
+ incident: {
+ summary: 'title',
+ description: 'desc',
+ labels: [],
+ issueType: '10006',
+ priority: 'High',
+ },
});
expect(res).toEqual({
@@ -225,7 +392,13 @@ describe('Jira service', () => {
await service.updateIncident({
incidentId: '1',
- incident: { summary: 'title', description: 'desc' },
+ incident: {
+ summary: 'title',
+ description: 'desc',
+ labels: [],
+ issueType: '10006',
+ priority: 'High',
+ },
});
expect(requestMock).toHaveBeenCalledWith({
@@ -233,22 +406,39 @@ describe('Jira service', () => {
logger,
method: 'put',
url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1',
- data: { fields: { summary: 'title', description: 'desc' } },
+ data: {
+ fields: {
+ summary: 'title',
+ description: 'desc',
+ labels: [],
+ priority: { name: 'High' },
+ issuetype: { id: '10006' },
+ project: { key: 'CK' },
+ },
+ },
});
});
test('it should throw an error', async () => {
requestMock.mockImplementation(() => {
- throw new Error('An error has occurred');
+ const error: ResponseError = new Error('An error has occurred');
+ error.response = { data: { errors: { summary: 'Required field' } } };
+ throw error;
});
expect(
service.updateIncident({
incidentId: '1',
- incident: { summary: 'title', description: 'desc' },
+ incident: {
+ summary: 'title',
+ description: 'desc',
+ labels: [],
+ issueType: '10006',
+ priority: 'High',
+ },
})
).rejects.toThrow(
- '[Action][Jira]: Unable to update incident with id 1. Error: An error has occurred'
+ '[Action][Jira]: Unable to update incident with id 1. Error: An error has occurred. Reason: Required field'
);
});
});
@@ -265,8 +455,14 @@ describe('Jira 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({
@@ -287,8 +483,14 @@ describe('Jira 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({
@@ -302,18 +504,416 @@ describe('Jira service', () => {
test('it should throw an error', async () => {
requestMock.mockImplementation(() => {
- throw new Error('An error has occurred');
+ const error: ResponseError = new Error('An error has occurred');
+ error.response = { data: { errors: { summary: 'Required field' } } };
+ throw error;
});
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][Jira]: Unable to create comment at incident with id 1. Error: An error has occurred'
+ '[Action][Jira]: Unable to create comment at incident with id 1. Error: An error has occurred. Reason: Required field'
+ );
+ });
+ });
+
+ describe('getCapabilities', () => {
+ test('it should return the capabilities', async () => {
+ requestMock.mockImplementation(() => ({
+ data: {
+ capabilities: {
+ navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation',
+ },
+ },
+ }));
+ const res = await service.getCapabilities();
+ expect(res).toEqual({
+ capabilities: {
+ navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation',
+ },
+ });
+ });
+
+ test('it should call request with correct arguments', async () => {
+ requestMock.mockImplementation(() => ({
+ data: {
+ capabilities: {
+ navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation',
+ },
+ },
+ }));
+
+ await service.getCapabilities();
+
+ expect(requestMock).toHaveBeenCalledWith({
+ axios,
+ logger,
+ method: 'get',
+ url: 'https://siem-kibana.atlassian.net/rest/capabilities',
+ });
+ });
+
+ test('it should throw an error', async () => {
+ requestMock.mockImplementation(() => {
+ const error: ResponseError = new Error('An error has occurred');
+ error.response = { data: { errors: { capabilities: 'Could not get capabilities' } } };
+ throw error;
+ });
+
+ expect(service.getCapabilities()).rejects.toThrow(
+ '[Action][Jira]: Unable to get capabilities. Error: An error has occurred. Reason: Could not get capabilities'
);
});
});
+
+ describe('getIssueTypes', () => {
+ describe('Old API', () => {
+ test('it should return the issue types', async () => {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ capabilities: {
+ navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation',
+ },
+ },
+ }));
+
+ requestMock.mockImplementationOnce(() => issueTypesResponse);
+
+ const res = await service.getIssueTypes();
+
+ expect(res).toEqual([
+ {
+ id: '10006',
+ name: 'Task',
+ },
+ {
+ id: '10007',
+ name: 'Bug',
+ },
+ ]);
+ });
+
+ test('it should call request with correct arguments', async () => {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ capabilities: {
+ navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation',
+ },
+ },
+ }));
+
+ requestMock.mockImplementationOnce(() => issueTypesResponse);
+
+ await service.getIssueTypes();
+
+ expect(requestMock).toHaveBeenLastCalledWith({
+ axios,
+ logger,
+ method: 'get',
+ url:
+ 'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta?projectKeys=CK&expand=projects.issuetypes.fields',
+ });
+ });
+
+ test('it should throw an error', async () => {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ capabilities: {
+ navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation',
+ },
+ },
+ }));
+
+ requestMock.mockImplementation(() => {
+ const error: ResponseError = new Error('An error has occurred');
+ error.response = { data: { errors: { issuetypes: 'Could not get issue types' } } };
+ throw error;
+ });
+
+ expect(service.getIssueTypes()).rejects.toThrow(
+ '[Action][Jira]: Unable to get issue types. Error: An error has occurred. Reason: Could not get issue types'
+ );
+ });
+ });
+ describe('New API', () => {
+ test('it should return the issue types', async () => {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ capabilities: {
+ 'list-project-issuetypes':
+ 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes',
+ 'list-issuetype-fields':
+ 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields',
+ },
+ },
+ }));
+
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ values: issueTypesResponse.data.projects[0].issuetypes,
+ },
+ }));
+
+ const res = await service.getIssueTypes();
+
+ expect(res).toEqual([
+ {
+ id: '10006',
+ name: 'Task',
+ },
+ {
+ id: '10007',
+ name: 'Bug',
+ },
+ ]);
+ });
+
+ test('it should call request with correct arguments', async () => {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ capabilities: {
+ 'list-project-issuetypes':
+ 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes',
+ 'list-issuetype-fields':
+ 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields',
+ },
+ },
+ }));
+
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ values: issueTypesResponse.data.projects[0].issuetypes,
+ },
+ }));
+
+ await service.getIssueTypes();
+
+ expect(requestMock).toHaveBeenLastCalledWith({
+ axios,
+ logger,
+ method: 'get',
+ url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta/CK/issuetypes',
+ });
+ });
+
+ test('it should throw an error', async () => {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ capabilities: {
+ 'list-project-issuetypes':
+ 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes',
+ 'list-issuetype-fields':
+ 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields',
+ },
+ },
+ }));
+
+ requestMock.mockImplementation(() => {
+ const error: ResponseError = new Error('An error has occurred');
+ error.response = { data: { errors: { issuetypes: 'Could not get issue types' } } };
+ throw error;
+ });
+
+ expect(service.getIssueTypes()).rejects.toThrow(
+ '[Action][Jira]: Unable to get issue types. Error: An error has occurred. Reason: Could not get issue types'
+ );
+ });
+ });
+ });
+
+ describe('getFieldsByIssueType', () => {
+ describe('Old API', () => {
+ test('it should return the fields', async () => {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ capabilities: {
+ navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation',
+ },
+ },
+ }));
+
+ requestMock.mockImplementationOnce(() => fieldsResponse);
+
+ const res = await service.getFieldsByIssueType('10006');
+
+ expect(res).toEqual({
+ priority: {
+ allowedValues: [
+ { id: '1', name: 'Highest' },
+ { id: '2', name: 'High' },
+ { id: '3', name: 'Medium' },
+ { id: '4', name: 'Low' },
+ { id: '5', name: 'Lowest' },
+ ],
+ defaultValue: { id: '3', name: 'Medium' },
+ },
+ summary: { allowedValues: [], defaultValue: {} },
+ });
+ });
+
+ test('it should call request with correct arguments', async () => {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ capabilities: {
+ navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation',
+ },
+ },
+ }));
+
+ requestMock.mockImplementationOnce(() => fieldsResponse);
+
+ await service.getFieldsByIssueType('10006');
+
+ expect(requestMock).toHaveBeenLastCalledWith({
+ axios,
+ logger,
+ method: 'get',
+ url:
+ 'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta?projectKeys=CK&issuetypeIds=10006&expand=projects.issuetypes.fields',
+ });
+ });
+
+ test('it should throw an error', async () => {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ capabilities: {
+ navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation',
+ },
+ },
+ }));
+
+ requestMock.mockImplementation(() => {
+ const error: ResponseError = new Error('An error has occurred');
+ error.response = { data: { errors: { fields: 'Could not get fields' } } };
+ throw error;
+ });
+
+ expect(service.getFieldsByIssueType('10006')).rejects.toThrow(
+ '[Action][Jira]: Unable to get fields. Error: An error has occurred. Reason: Could not get fields'
+ );
+ });
+ });
+
+ describe('New API', () => {
+ test('it should return the fields', async () => {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ capabilities: {
+ 'list-project-issuetypes':
+ 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes',
+ 'list-issuetype-fields':
+ 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields',
+ },
+ },
+ }));
+
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ values: [
+ { fieldId: 'summary' },
+ {
+ fieldId: 'priority',
+ allowedValues: [
+ {
+ name: 'Medium',
+ id: '3',
+ },
+ ],
+ defaultValue: {
+ name: 'Medium',
+ id: '3',
+ },
+ },
+ ],
+ },
+ }));
+
+ const res = await service.getFieldsByIssueType('10006');
+
+ expect(res).toEqual({
+ priority: {
+ allowedValues: [{ id: '3', name: 'Medium' }],
+ defaultValue: { id: '3', name: 'Medium' },
+ },
+ summary: { allowedValues: [], defaultValue: {} },
+ });
+ });
+
+ test('it should call request with correct arguments', async () => {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ capabilities: {
+ 'list-project-issuetypes':
+ 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes',
+ 'list-issuetype-fields':
+ 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields',
+ },
+ },
+ }));
+
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ values: [
+ { fieldId: 'summary' },
+ {
+ fieldId: 'priority',
+ allowedValues: [
+ {
+ name: 'Medium',
+ id: '3',
+ },
+ ],
+ defaultValue: {
+ name: 'Medium',
+ id: '3',
+ },
+ },
+ ],
+ },
+ }));
+
+ await service.getFieldsByIssueType('10006');
+
+ expect(requestMock).toHaveBeenLastCalledWith({
+ axios,
+ logger,
+ method: 'get',
+ url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta/CK/issuetypes/10006',
+ });
+ });
+
+ test('it should throw an error', async () => {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ capabilities: {
+ 'list-project-issuetypes':
+ 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes',
+ 'list-issuetype-fields':
+ 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields',
+ },
+ },
+ }));
+
+ requestMock.mockImplementation(() => {
+ const error: ResponseError = new Error('An error has occurred');
+ error.response = { data: { errors: { issuetypes: 'Could not get issue types' } } };
+ throw error;
+ });
+
+ expect(service.getFieldsByIssueType('10006')).rejects.toThrow(
+ '[Action][Jira]: Unable to get fields. Error: An error has occurred. Reason: Could not get issue types'
+ );
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts
index aec73cfb375ed..84b6e70d2a100 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts
@@ -6,14 +6,20 @@
import axios from 'axios';
-import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types';
import { Logger } from '../../../../../../src/core/server';
import {
+ ExternalServiceCredentials,
+ ExternalService,
+ CreateIncidentParams,
+ UpdateIncidentParams,
JiraPublicConfigurationType,
JiraSecretConfigurationType,
- CreateIncidentRequest,
- UpdateIncidentRequest,
- CreateCommentRequest,
+ Fields,
+ CreateCommentParams,
+ Incident,
+ ResponseError,
+ ExternalServiceCommentResponse,
+ ExternalServiceIncidentResponse,
} from './types';
import * as i18n from './translations';
@@ -22,11 +28,12 @@ import { ProxySettings } from '../../types';
const VERSION = '2';
const BASE_URL = `rest/api/${VERSION}`;
-const INCIDENT_URL = `issue`;
-const COMMENT_URL = `comment`;
+const CAPABILITIES_URL = `rest/capabilities`;
const VIEW_INCIDENT_URL = `browse`;
+const createMetaCapabilities = ['list-project-issuetypes', 'list-issuetype-fields'];
+
export const createExternalService = (
{ config, secrets }: ExternalServiceCredentials,
logger: Logger,
@@ -39,8 +46,13 @@ export const createExternalService = (
throw Error(`[Action]${i18n.NAME}: Wrong configuration.`);
}
- const incidentUrl = `${url}/${BASE_URL}/${INCIDENT_URL}`;
- const commentUrl = `${incidentUrl}/{issueId}/${COMMENT_URL}`;
+ const incidentUrl = `${url}/${BASE_URL}/issue`;
+ const capabilitiesUrl = `${url}/${CAPABILITIES_URL}`;
+ const commentUrl = `${incidentUrl}/{issueId}/comment`;
+ const getIssueTypesOldAPIURL = `${url}/${BASE_URL}/issue/createmeta?projectKeys=${projectKey}&expand=projects.issuetypes.fields`;
+ const getIssueTypeFieldsOldAPIURL = `${url}/${BASE_URL}/issue/createmeta?projectKeys=${projectKey}&issuetypeIds={issueTypeId}&expand=projects.issuetypes.fields`;
+ const getIssueTypesUrl = `${url}/${BASE_URL}/issue/createmeta/${projectKey}/issuetypes`;
+ const getIssueTypeFieldsUrl = `${url}/${BASE_URL}/issue/createmeta/${projectKey}/issuetypes/{issueTypeId}`;
const axiosInstance = axios.create({
auth: { username: email, password: apiToken },
});
@@ -52,6 +64,60 @@ export const createExternalService = (
const getCommentsURL = (issueId: string) => {
return commentUrl.replace('{issueId}', issueId);
};
+ const createGetIssueTypeFieldsUrl = (uri: string, issueTypeId: string) => {
+ return uri.replace('{issueTypeId}', issueTypeId);
+ };
+
+ const createFields = (key: string, incident: Incident): Fields => {
+ let fields: Fields = {
+ summary: incident.summary,
+ project: { key },
+ };
+
+ if (incident.issueType) {
+ fields = { ...fields, issuetype: { id: incident.issueType } };
+ }
+
+ if (incident.description) {
+ fields = { ...fields, description: incident.description };
+ }
+
+ if (incident.labels) {
+ fields = { ...fields, labels: incident.labels };
+ }
+
+ if (incident.priority) {
+ fields = { ...fields, priority: { name: incident.priority } };
+ }
+
+ return fields;
+ };
+
+ const createErrorMessage = (errors: ResponseError) => {
+ return Object.entries(errors).reduce((errorMessage, [, value]) => {
+ const msg = errorMessage.length > 0 ? `${errorMessage} ${value}` : value;
+ return msg;
+ }, '');
+ };
+
+ const hasSupportForNewAPI = (capabilities: { capabilities?: {} }) =>
+ createMetaCapabilities.every((c) => Object.keys(capabilities?.capabilities ?? {}).includes(c));
+
+ const normalizeIssueTypes = (issueTypes: Array<{ id: string; name: string }>) =>
+ issueTypes.map((type) => ({ id: type.id, name: type.name }));
+
+ const normalizeFields = (fields: {
+ [key: string]: { allowedValues?: Array<{}>; defaultValue?: {} };
+ }) =>
+ Object.keys(fields ?? {}).reduce((fieldsAcc, fieldKey) => {
+ return {
+ ...fieldsAcc,
+ [fieldKey]: {
+ allowedValues: fields[fieldKey]?.allowedValues ?? [],
+ defaultValue: fields[fieldKey]?.defaultValue ?? {},
+ },
+ };
+ }, {});
const getIncident = async (id: string) => {
try {
@@ -67,23 +133,46 @@ export const createExternalService = (
return { ...rest, ...fields };
} 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
+ } Reason: ${createErrorMessage(error.response?.data?.errors ?? {})}`
+ )
);
}
};
- const createIncident = async ({ incident }: ExternalServiceParams) => {
- // The response from Jira when creating an issue contains only the key and the id.
- // The function makes two calls when creating an issue. One to create the issue and one to get
- // the created issue with all the necessary fields.
+ const createIncident = async ({
+ incident,
+ }: CreateIncidentParams): Promise => {
+ /* The response from Jira when creating an issue contains only the key and the id.
+ The function makes the following calls when creating an issue:
+ 1. Get issueTypes to set a default ONLY when incident.issueType is missing
+ 2. Create the issue.
+ 3. Get the created issue with all the necessary fields.
+ */
+
+ let issueType = incident.issueType;
+
+ if (!incident.issueType) {
+ const issueTypes = await getIssueTypes();
+ issueType = issueTypes[0]?.id ?? '';
+ }
+
+ const fields = createFields(projectKey, {
+ ...incident,
+ issueType,
+ });
+
try {
- const res = await request({
+ const res = await request({
axios: axiosInstance,
url: `${incidentUrl}`,
logger,
method: 'post',
data: {
- fields: { ...incident, project: { key: projectKey }, issuetype: { name: 'Task' } },
+ fields,
},
proxySettings,
});
@@ -98,23 +187,38 @@ 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}. Reason: ${createErrorMessage(
+ error.response?.data?.errors ?? {}
+ )}`
+ )
);
}
};
- const updateIncident = async ({ incidentId, incident }: ExternalServiceParams) => {
+ const updateIncident = async ({
+ incidentId,
+ incident,
+ }: UpdateIncidentParams): Promise => {
+ const incidentWithoutNullValues = Object.entries(incident).reduce(
+ (obj, [key, value]) => (value != null ? { ...obj, [key]: value } : obj),
+ {} as Incident
+ );
+
+ const fields = createFields(projectKey, incidentWithoutNullValues);
+
try {
- await request({
+ await request({
axios: axiosInstance,
method: 'put',
url: `${incidentUrl}/${incidentId}`,
logger,
- data: { fields: { ...incident } },
+ data: { fields },
proxySettings,
});
- const updatedIncident = await getIncident(incidentId);
+ const updatedIncident = await getIncident(incidentId as string);
return {
title: updatedIncident.key,
@@ -126,15 +230,20 @@ export const createExternalService = (
throw new Error(
getErrorMessage(
i18n.NAME,
- `Unable to update incident with id ${incidentId}. Error: ${error.message}`
+ `Unable to update incident with id ${incidentId}. Error: ${
+ error.message
+ }. Reason: ${createErrorMessage(error.response?.data?.errors ?? {})}`
)
);
}
};
- const createComment = async ({ incidentId, comment, field }: ExternalServiceParams) => {
+ const createComment = async ({
+ incidentId,
+ comment,
+ }: CreateCommentParams): Promise => {
try {
- const res = await request({
+ const res = await request({
axios: axiosInstance,
method: 'post',
url: getCommentsURL(incidentId),
@@ -152,7 +261,118 @@ 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
+ }. Reason: ${createErrorMessage(error.response?.data?.errors ?? {})}`
+ )
+ );
+ }
+ };
+
+ const getCapabilities = async () => {
+ try {
+ const res = await request({
+ axios: axiosInstance,
+ method: 'get',
+ url: capabilitiesUrl,
+ logger,
+ proxySettings,
+ });
+
+ return { ...res.data };
+ } catch (error) {
+ throw new Error(
+ getErrorMessage(
+ i18n.NAME,
+ `Unable to get capabilities. Error: ${error.message}. Reason: ${createErrorMessage(
+ error.response?.data?.errors ?? {}
+ )}`
+ )
+ );
+ }
+ };
+
+ const getIssueTypes = async () => {
+ const capabilitiesResponse = await getCapabilities();
+ const supportsNewAPI = hasSupportForNewAPI(capabilitiesResponse);
+
+ try {
+ if (!supportsNewAPI) {
+ const res = await request({
+ axios: axiosInstance,
+ method: 'get',
+ url: getIssueTypesOldAPIURL,
+ logger,
+ proxySettings,
+ });
+
+ const issueTypes = res.data.projects[0]?.issuetypes ?? [];
+ return normalizeIssueTypes(issueTypes);
+ } else {
+ const res = await request({
+ axios: axiosInstance,
+ method: 'get',
+ url: getIssueTypesUrl,
+ logger,
+ proxySettings,
+ });
+
+ const issueTypes = res.data.values;
+ return normalizeIssueTypes(issueTypes);
+ }
+ } catch (error) {
+ throw new Error(
+ getErrorMessage(
+ i18n.NAME,
+ `Unable to get issue types. Error: ${error.message}. Reason: ${createErrorMessage(
+ error.response?.data?.errors ?? {}
+ )}`
+ )
+ );
+ }
+ };
+
+ const getFieldsByIssueType = async (issueTypeId: string) => {
+ const capabilitiesResponse = await getCapabilities();
+ const supportsNewAPI = hasSupportForNewAPI(capabilitiesResponse);
+
+ try {
+ if (!supportsNewAPI) {
+ const res = await request({
+ axios: axiosInstance,
+ method: 'get',
+ url: createGetIssueTypeFieldsUrl(getIssueTypeFieldsOldAPIURL, issueTypeId),
+ logger,
+ proxySettings,
+ });
+
+ const fields = res.data.projects[0]?.issuetypes[0]?.fields || {};
+ return normalizeFields(fields);
+ } else {
+ const res = await request({
+ axios: axiosInstance,
+ method: 'get',
+ url: createGetIssueTypeFieldsUrl(getIssueTypeFieldsUrl, issueTypeId),
+ logger,
+ proxySettings,
+ });
+
+ const fields = res.data.values.reduce(
+ (acc: { [x: string]: {} }, value: { fieldId: string }) => ({
+ ...acc,
+ [value.fieldId]: { ...value },
+ }),
+ {}
+ );
+ return normalizeFields(fields);
+ }
+ } catch (error) {
+ throw new Error(
+ getErrorMessage(
+ i18n.NAME,
+ `Unable to get fields. Error: ${error.message}. Reason: ${createErrorMessage(
+ error.response?.data?.errors ?? {}
+ )}`
)
);
}
@@ -163,5 +383,8 @@ export const createExternalService = (
createIncident,
updateIncident,
createComment,
+ getCapabilities,
+ getIssueTypes,
+ getFieldsByIssueType,
};
};
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts
index dae0d75952e11..0e71de813eb5d 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts
@@ -9,3 +9,19 @@ import { i18n } from '@kbn/i18n';
export const NAME = i18n.translate('xpack.actions.builtin.case.jiraTitle', {
defaultMessage: 'Jira',
});
+
+export const ALLOWED_HOSTS_ERROR = (message: string) =>
+ i18n.translate('xpack.actions.builtin.jira.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.jira.configuration.emptyMapping',
+ {
+ defaultMessage: '[incidentConfiguration.mapping]: expected non-empty but got empty',
+ }
+);
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts
index 8d9c6b92abb3b..5e97f5309f8ee 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts
@@ -4,29 +4,169 @@
* 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 { JiraPublicConfigurationSchema, JiraSecretConfigurationSchema } from './schema';
+import {
+ ExternalIncidentServiceConfigurationSchema,
+ ExternalIncidentServiceSecretConfigurationSchema,
+ ExecutorParamsSchema,
+ ExecutorSubActionPushParamsSchema,
+ ExecutorSubActionGetIncidentParamsSchema,
+ ExecutorSubActionHandshakeParamsSchema,
+ ExecutorSubActionGetCapabilitiesParamsSchema,
+ ExecutorSubActionGetIssueTypesParamsSchema,
+ ExecutorSubActionGetFieldsByIssueTypeParamsSchema,
+} from './schema';
+import { ActionsConfigurationUtilities } from '../../actions_config';
+import { IncidentConfigurationSchema } from '../case/common_schema';
+import { Comment } from '../case/common_types';
+import { Logger } from '../../../../../../src/core/server';
+
+export type JiraPublicConfigurationType = TypeOf;
+export type JiraSecretConfigurationType = TypeOf<
+ typeof ExternalIncidentServiceSecretConfigurationSchema
+>;
+
+export type ExecutorParams = TypeOf;
+export type ExecutorSubActionPushParams = TypeOf;
+
+export type IncidentConfiguration = TypeOf;
+
+export interface ExternalServiceCredentials {
+ config: Record;
+ secrets: Record;
+}
+
+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;
+
+export type Incident = Pick<
+ ExecutorSubActionPushParams,
+ 'description' | 'priority' | 'labels' | 'issueType'
+> & { summary: string };
+
+export interface CreateIncidentParams {
+ incident: Incident;
+}
+
+export interface UpdateIncidentParams {
+ incidentId: string;
+ incident: Incident;
+}
+
+export interface CreateCommentParams {
+ incidentId: string;
+ comment: Comment;
+}
-export type JiraPublicConfigurationType = TypeOf;
-export type JiraSecretConfigurationType = TypeOf;
+export type GetIssueTypesResponse = Array<{ id: string; name: string }>;
+export type GetFieldsByIssueTypeResponse = Record<
+ string,
+ { allowedValues: Array<{}>; defaultValue: {} }
+>;
-interface CreateIncidentBasicRequestArgs {
- summary: string;
- description: string;
+export interface ExternalService {
+ getIncident: (id: string) => Promise;
+ createIncident: (params: CreateIncidentParams) => Promise;
+ updateIncident: (params: UpdateIncidentParams) => Promise;
+ createComment: (params: CreateCommentParams) => Promise;
+ getCapabilities: () => Promise;
+ getIssueTypes: () => Promise;
+ getFieldsByIssueType: (issueTypeId: string) => Promise;
}
-interface CreateIncidentRequestArgs extends CreateIncidentBasicRequestArgs {
- project: { key: string };
- issuetype: { name: string };
+
+export interface PushToServiceApiParams extends ExecutorSubActionPushParams {
+ externalObject: Record;
+}
+
+export type ExecutorSubActionGetIncidentParams = TypeOf<
+ typeof ExecutorSubActionGetIncidentParamsSchema
+>;
+
+export type ExecutorSubActionHandshakeParams = TypeOf<
+ typeof ExecutorSubActionHandshakeParamsSchema
+>;
+
+export type ExecutorSubActionGetCapabilitiesParams = TypeOf<
+ typeof ExecutorSubActionGetCapabilitiesParamsSchema
+>;
+
+export type ExecutorSubActionGetIssueTypesParams = TypeOf<
+ typeof ExecutorSubActionGetIssueTypesParamsSchema
+>;
+
+export type ExecutorSubActionGetFieldsByIssueTypeParams = TypeOf<
+ typeof ExecutorSubActionGetFieldsByIssueTypeParamsSchema
+>;
+
+export interface ExternalServiceApiHandlerArgs {
+ externalService: ExternalService;
+ mapping: Map | null;
+}
+
+export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs {
+ params: PushToServiceApiParams;
+ logger: Logger;
}
-export interface CreateIncidentRequest {
- fields: CreateIncidentRequestArgs;
+export interface GetIncidentApiHandlerArgs extends ExternalServiceApiHandlerArgs {
+ params: ExecutorSubActionGetIncidentParams;
}
-export interface UpdateIncidentRequest {
- fields: Partial;
+export interface HandshakeApiHandlerArgs extends ExternalServiceApiHandlerArgs {
+ params: ExecutorSubActionHandshakeParams;
}
-export interface CreateCommentRequest {
- body: string;
+export interface GetIssueTypesHandlerArgs {
+ externalService: ExternalService;
+ params: ExecutorSubActionGetIssueTypesParams;
+}
+
+export interface GetFieldsByIssueTypeHandlerArgs {
+ externalService: ExternalService;
+ params: ExecutorSubActionGetFieldsByIssueTypeParams;
+}
+
+export interface PushToServiceResponse extends ExternalServiceIncidentResponse {
+ comments?: ExternalServiceCommentResponse[];
+}
+
+export interface ExternalServiceApi {
+ handshake: (args: HandshakeApiHandlerArgs) => Promise;
+ pushToService: (args: PushToServiceApiHandlerArgs) => Promise;
+ getIncident: (args: GetIncidentApiHandlerArgs) => Promise;
+ issueTypes: (args: GetIssueTypesHandlerArgs) => Promise;
+ fieldsByIssueType: (
+ args: GetFieldsByIssueTypeHandlerArgs
+ ) => Promise;
+}
+
+export type JiraExecutorResultData =
+ | PushToServiceResponse
+ | GetIssueTypesResponse
+ | GetFieldsByIssueTypeResponse;
+
+export interface Fields {
+ [key: string]: string | string[] | { name: string } | { key: string } | { id: string };
+}
+export interface ResponseError {
+ [k: string]: string;
}
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts
index 7226071392bc6..58a3e27247fae 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts
@@ -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 {
+ JiraPublicConfigurationType,
+ JiraSecretConfigurationType,
+ ExternalServiceValidation,
+} from './types';
+
+import * as i18n from './translations';
+
+export const validateCommonConfig = (
+ configurationUtilities: ActionsConfigurationUtilities,
+ configObject: JiraPublicConfigurationType
+) => {
+ 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: JiraSecretConfigurationType
+) => {};
export const validate: ExternalServiceValidation = {
config: validateCommonConfig,
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts
index 0bb096ecd0f62..7a68781bb9a75 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts
@@ -91,7 +91,7 @@ describe('api', () => {
expect(externalService.updateIncident).not.toHaveBeenCalled();
});
- test('it calls updateIncident correctly', async () => {
+ test('it calls updateIncident correctly when creating an incident and having comments', async () => {
const params = { ...apiParams, externalId: null };
await api.pushToService({
externalService,
@@ -103,7 +103,7 @@ describe('api', () => {
expect(externalService.updateIncident).toHaveBeenCalledTimes(2);
expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, {
incident: {
- comments: 'A comment',
+ comments: 'A comment (added at 2020-03-13T08:34:53.450Z by Elastic User)',
description:
'Incident description (created at 2020-03-13T08:34:53.450Z by Elastic User)',
short_description:
@@ -114,7 +114,7 @@ describe('api', () => {
expect(externalService.updateIncident).toHaveBeenNthCalledWith(2, {
incident: {
- comments: 'Another comment',
+ comments: 'Another comment (added at 2020-03-13T08:34:53.450Z by Elastic User)',
description:
'Incident description (created at 2020-03-13T08:34:53.450Z by Elastic User)',
short_description:
@@ -215,7 +215,7 @@ describe('api', () => {
expect(externalService.updateIncident).toHaveBeenNthCalledWith(2, {
incident: {
- comments: 'A comment',
+ comments: 'A comment (added at 2020-03-13T08:34:53.450Z by Elastic User)',
description:
'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
short_description:
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts
index 3281832941558..c8e6147ecef46 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts
@@ -10,11 +10,13 @@ import {
HandshakeApiHandlerArgs,
GetIncidentApiHandlerArgs,
ExternalServiceApi,
+ PushToServiceApiParams,
+ PushToServiceResponse,
} from './types';
// TODO: to remove, need to support Case
import { transformers } from '../case/transformers';
-import { PushToServiceResponse, TransformFieldsArgs } from './case_types';
+import { TransformFieldsArgs, Comment, EntityInformation } from '../case/common_types';
import { prepareFieldsForTransformation } from '../case/utils';
const handshakeHandler = async ({
@@ -92,9 +94,10 @@ const pushToServiceHandler = async ({
mapping.get('comments')?.actionType !== 'nothing'
) {
res.comments = [];
+ const commentsTransformed = transformComments(comments, ['informationAdded']);
const fieldsKey = mapping.get('comments')?.target ?? 'comments';
- for (const currentComment of comments) {
+ for (const currentComment of commentsTransformed) {
await externalService.updateIncident({
incidentId: res.id,
incident: {
@@ -118,7 +121,7 @@ export const transformFields = ({
params,
fields,
currentIncident,
-}: TransformFieldsArgs): Record => {
+}: TransformFieldsArgs): Record => {
return fields.reduce((prev, cur) => {
const transform = flow(...cur.pipes.map((p) => transformers[p]));
return {
@@ -126,20 +129,35 @@ 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,
};
}, {});
};
+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,
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts
index 3addbe7c54dac..41a577918b18e 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts
@@ -24,11 +24,11 @@ import {
ExecutorSubActionPushParams,
ServiceNowPublicConfigurationType,
ServiceNowSecretConfigurationType,
+ PushToServiceResponse,
} from './types';
// TODO: to remove, need to support Case
import { buildMap, mapParams } from '../case/utils';
-import { PushToServiceResponse } from './case_types';
interface GetActionTypeParams {
logger: Logger;
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts
index 5f22fcd4fdc85..55a14e4528acf 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts
@@ -5,7 +5,7 @@
*/
import { ExternalService, PushToServiceApiParams, ExecutorSubActionPushParams } from './types';
-import { MapRecord } from './case_types';
+import { MapRecord } from '../case/common_types';
const createMock = (): jest.Mocked => {
const service = {
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts
index 82afebaaee445..921de42adfcaf 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts
@@ -5,7 +5,11 @@
*/
import { schema } from '@kbn/config-schema';
-import { CommentSchema, EntityInformation, IncidentConfigurationSchema } from './case_shema';
+import {
+ CommentSchema,
+ EntityInformation,
+ IncidentConfigurationSchema,
+} from '../case/common_schema';
export const ExternalIncidentServiceConfiguration = {
apiUrl: schema.string(),
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts
index 05c7d805a1852..7cc97a241c4bc 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts
@@ -10,8 +10,8 @@ export const NAME = i18n.translate('xpack.actions.builtin.servicenowTitle', {
defaultMessage: 'ServiceNow',
});
-export const WHITE_LISTED_ERROR = (message: string) =>
- i18n.translate('xpack.actions.builtin.configuration.apiWhitelistError', {
+export const ALLOWED_HOSTS_ERROR = (message: string) =>
+ i18n.translate('xpack.actions.builtin.configuration.apiAllowedHostsError', {
defaultMessage: 'error configuring connector action: {message}',
values: {
message,
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts
index 0db9b6642ea5c..e8fcfac45d789 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts
@@ -16,8 +16,8 @@ import {
ExecutorSubActionHandshakeParamsSchema,
} from './schema';
import { ActionsConfigurationUtilities } from '../../actions_config';
-import { IncidentConfigurationSchema } from './case_shema';
-import { PushToServiceResponse } from './case_types';
+import { ExternalServiceCommentResponse } from '../case/common_types';
+import { IncidentConfigurationSchema } from '../case/common_schema';
import { Logger } from '../../../../../../src/core/server';
export type ServiceNowPublicConfigurationType = TypeOf<
@@ -52,6 +52,9 @@ export interface ExternalServiceIncidentResponse {
url: string;
pushedDate: string;
}
+export interface PushToServiceResponse extends ExternalServiceIncidentResponse {
+ comments?: ExternalServiceCommentResponse[];
+}
export type ExternalServiceParams = Record;
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts
index 6eec3b8d63b86..87bbfd9c7ea95 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts
@@ -27,8 +27,8 @@ export const validateCommonConfig = (
try {
configurationUtilities.ensureUriAllowed(configObject.apiUrl);
- } catch (allowListError) {
- return i18n.WHITE_LISTED_ERROR(allowListError.message);
+ } catch (allowedListError) {
+ return i18n.ALLOWED_HOSTS_ERROR(allowedListError.message);
}
};
diff --git a/x-pack/plugins/case/common/constants.ts b/x-pack/plugins/case/common/constants.ts
index bd12c258a5388..15a318002390f 100644
--- a/x-pack/plugins/case/common/constants.ts
+++ b/x-pack/plugins/case/common/constants.ts
@@ -28,5 +28,11 @@ export const CASE_USER_ACTIONS_URL = `${CASE_DETAILS_URL}/user_actions`;
export const ACTION_URL = '/api/actions';
export const ACTION_TYPES_URL = '/api/actions/list_action_types';
export const SERVICENOW_ACTION_TYPE_ID = '.servicenow';
+export const JIRA_ACTION_TYPE_ID = '.jira';
+export const RESILIENT_ACTION_TYPE_ID = '.resilient';
-export const SUPPORTED_CONNECTORS = ['.servicenow', '.jira', '.resilient'];
+export const SUPPORTED_CONNECTORS = [
+ SERVICENOW_ACTION_TYPE_ID,
+ JIRA_ACTION_TYPE_ID,
+ RESILIENT_ACTION_TYPE_ID,
+];
diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts
index 28e75dd2f8c32..a22d7ae5cea21 100644
--- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts
@@ -12,6 +12,7 @@ import {
CASE_CONFIGURE_CONNECTORS_URL,
SUPPORTED_CONNECTORS,
SERVICENOW_ACTION_TYPE_ID,
+ JIRA_ACTION_TYPE_ID,
} from '../../../../../common/constants';
/*
@@ -36,8 +37,9 @@ export function initCaseConfigureGetActionConnector({ caseService, router }: Rou
(action) =>
SUPPORTED_CONNECTORS.includes(action.actionTypeId) &&
// Need this filtering temporary to display only Case owned ServiceNow connectors
- (action.actionTypeId !== SERVICENOW_ACTION_TYPE_ID ||
- (action.actionTypeId === SERVICENOW_ACTION_TYPE_ID && action.config!.isCaseOwned))
+ (![SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID].includes(action.actionTypeId) ||
+ ([SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID].includes(action.actionTypeId) &&
+ action.config?.isCaseOwned === true))
);
return response.ok({ body: results });
} catch (error) {
diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts
index 833f85712b5fa..9e6982ea20301 100644
--- a/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts
+++ b/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts
@@ -4,14 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { ServiceNowConnectorConfiguration } from '../../../../../triggers_actions_ui/public/common';
-import { connector as jiraConnectorConfig } from './jira/config';
+/* eslint-disable @kbn/eslint/no-restricted-paths */
+
+import {
+ ServiceNowConnectorConfiguration,
+ JiraConnectorConfiguration,
+} from '../../../../../triggers_actions_ui/public/common';
import { connector as resilientConnectorConfig } from './resilient/config';
import { ConnectorConfiguration } from './types';
export const connectorsConfiguration: Record = {
'.servicenow': ServiceNowConnectorConfiguration as ConnectorConfiguration,
- '.jira': jiraConnectorConfig,
+ '.jira': JiraConnectorConfiguration as ConnectorConfiguration,
'.resilient': resilientConnectorConfig,
};
diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts
index f32e1e0df184e..33afa82c84f34 100644
--- a/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts
+++ b/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts
@@ -4,5 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { getActionType as jiraActionType } from './jira';
export { getActionType as resilientActionType } from './resilient';
diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/flyout.tsx b/x-pack/plugins/security_solution/public/common/lib/connectors/jira/flyout.tsx
deleted file mode 100644
index 0737db3cd08eb..0000000000000
--- a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/flyout.tsx
+++ /dev/null
@@ -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 { JiraActionConnector } from './types';
-import { withConnectorFlyout } from '../components/connector_flyout';
-
-const JiraConnectorForm: React.FC> = ({
- errors,
- action,
- onChangeSecret,
- onBlurSecret,
- onChangeConfig,
- onBlurConfig,
-}) => {
- const { projectKey } = action.config;
- const { email, apiToken } = action.secrets;
- const isProjectKeyInvalid: boolean = errors.projectKey.length > 0 && projectKey != null;
- const isEmailInvalid: boolean = errors.email.length > 0 && email != null;
- const isApiTokenInvalid: boolean = errors.apiToken.length > 0 && apiToken != null;
-
- return (
- <>
-
-
-
- onChangeConfig('projectKey', evt.target.value)}
- onBlur={() => onBlurConfig('projectKey')}
- />
-
-
-
-
-
-
-
- onChangeSecret('email', evt.target.value)}
- onBlur={() => onBlurSecret('email')}
- />
-
-
-
-
-
-
-
- onChangeSecret('apiToken', evt.target.value)}
- onBlur={() => onBlurSecret('apiToken')}
- />
-
-
-
- >
- );
-};
-
-export const JiraConnectorFlyout = withConnectorFlyout({
- ConnectorFormComponent: JiraConnectorForm,
- secretKeys: ['email', 'apiToken'],
- configKeys: ['projectKey'],
- connectorActionTypeId: '.jira',
-});
-
-// eslint-disable-next-line import/no-default-export
-export { JiraConnectorFlyout as default };
diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/index.tsx b/x-pack/plugins/security_solution/public/common/lib/connectors/jira/index.tsx
deleted file mode 100644
index cead392010dc7..0000000000000
--- a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/index.tsx
+++ /dev/null
@@ -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 { JiraActionConnector } from './types';
-import * as i18n from './translations';
-
-interface Errors {
- projectKey: string[];
- email: string[];
- apiToken: string[];
-}
-
-const validateConnector = (action: JiraActionConnector): ValidationResult => {
- const errors: Errors = {
- projectKey: [],
- email: [],
- apiToken: [],
- };
-
- if (!action.config.projectKey) {
- errors.projectKey = [...errors.projectKey, i18n.JIRA_PROJECT_KEY_REQUIRED];
- }
-
- if (!action.secrets.email) {
- errors.email = [...errors.email, i18n.JIRA_EMAIL_REQUIRED];
- }
-
- if (!action.secrets.apiToken) {
- errors.apiToken = [...errors.apiToken, i18n.JIRA_API_TOKEN_REQUIRED];
- }
-
- return { errors };
-};
-
-export const getActionType = createActionType({
- id: connector.id,
- iconClass: logo,
- selectMessage: i18n.JIRA_DESC,
- actionTypeTitle: connector.name,
- validateConnector,
- actionConnectorFields: lazy(() => import('./flyout')),
-});
diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/translations.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/jira/translations.ts
deleted file mode 100644
index d7abf77a58d4c..0000000000000
--- a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/translations.ts
+++ /dev/null
@@ -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 JIRA_DESC = i18n.translate(
- 'xpack.securitySolution.case.connectors.jira.selectMessageText',
- {
- defaultMessage: 'Push or update Security case data to a new issue in Jira',
- }
-);
-
-export const JIRA_TITLE = i18n.translate(
- 'xpack.securitySolution.case.connectors.jira.actionTypeTitle',
- {
- defaultMessage: 'Jira',
- }
-);
-
-export const JIRA_PROJECT_KEY_LABEL = i18n.translate(
- 'xpack.securitySolution.case.connectors.jira.projectKey',
- {
- defaultMessage: 'Project key',
- }
-);
-
-export const JIRA_PROJECT_KEY_REQUIRED = i18n.translate(
- 'xpack.securitySolution.case.connectors.jira.requiredProjectKeyTextField',
- {
- defaultMessage: 'Project key is required',
- }
-);
-
-export const JIRA_EMAIL_LABEL = i18n.translate(
- 'xpack.securitySolution.case.connectors.jira.emailTextFieldLabel',
- {
- defaultMessage: 'Email or Username',
- }
-);
-
-export const JIRA_EMAIL_REQUIRED = i18n.translate(
- 'xpack.securitySolution.case.connectors.jira.requiredEmailTextField',
- {
- defaultMessage: 'Email or Username is required',
- }
-);
-
-export const JIRA_API_TOKEN_LABEL = i18n.translate(
- 'xpack.securitySolution.case.connectors.jira.apiTokenTextFieldLabel',
- {
- defaultMessage: 'API token or Password',
- }
-);
-
-export const JIRA_API_TOKEN_REQUIRED = i18n.translate(
- 'xpack.securitySolution.case.connectors.jira.requiredApiTokenTextField',
- {
- defaultMessage: 'API token or Password is required',
- }
-);
-
-export const MAPPING_FIELD_SUMMARY = i18n.translate(
- 'xpack.securitySolution.case.configureCases.mappingFieldSummary',
- {
- defaultMessage: 'Summary',
- }
-);
diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/types.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/jira/types.ts
deleted file mode 100644
index fafb4a0d41fb3..0000000000000
--- a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/types.ts
+++ /dev/null
@@ -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 {
- JiraPublicConfigurationType,
- JiraSecretConfigurationType,
-} from '../../../../../../actions/server/builtin_action_types/jira/types';
-
-export { JiraFieldsType } from '../../../../../../case/common/api/connectors';
-
-export * from '../types';
-
-export interface JiraActionConnector {
- config: JiraPublicConfigurationType;
- secrets: JiraSecretConfigurationType;
-}
diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx
index 1017cbb6a2c61..10bbbbfa72719 100644
--- a/x-pack/plugins/security_solution/public/plugin.tsx
+++ b/x-pack/plugins/security_solution/public/plugin.tsx
@@ -21,7 +21,7 @@ import {
import { Storage } from '../../../../src/plugins/kibana_utils/public';
import { initTelemetry } from './common/lib/telemetry';
import { KibanaServices } from './common/lib/kibana/services';
-import { jiraActionType, resilientActionType } from './common/lib/connectors';
+import { resilientActionType } from './common/lib/connectors';
import {
PluginSetup,
PluginStart,
@@ -96,7 +96,6 @@ export class Plugin implements IPlugin {
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 89d400f92bce9..f753e0ec87064 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -4445,7 +4445,6 @@
"xpack.actions.builtin.case.connectorApiNullError": "コネクター[apiUrl]が必要です",
"xpack.actions.builtin.case.jiraTitle": "Jira",
"xpack.actions.builtin.case.resilientTitle": "IBM Resilient",
- "xpack.actions.builtin.configuration.apiWhitelistError": "コネクターアクションの構成エラー:{message}",
"xpack.actions.builtin.email.errorSendingErrorMessage": "エラー送信メールアドレス",
"xpack.actions.builtin.emailTitle": "メール",
"xpack.actions.builtin.esIndex.errorIndexingErrorMessage": "エラーインデックス作成ドキュメント",
@@ -15114,7 +15113,6 @@
"xpack.securitySolution.case.configureCases.mappingFieldDescription": "説明",
"xpack.securitySolution.case.configureCases.mappingFieldName": "名前",
"xpack.securitySolution.case.configureCases.mappingFieldNotMapped": "マップされません",
- "xpack.securitySolution.case.configureCases.mappingFieldSummary": "まとめ",
"xpack.securitySolution.case.configureCases.noConnector": "コネクターを選択していません",
"xpack.securitySolution.case.configureCases.updateConnector": "コネクターを更新",
"xpack.securitySolution.case.configureCases.updateSelectedConnector": "{ connectorName }を更新",
@@ -15138,14 +15136,6 @@
"xpack.securitySolution.case.connectors.common.requiredPasswordTextField": "パスワードが必要です",
"xpack.securitySolution.case.connectors.common.requiredUsernameTextField": "ユーザー名が必要です",
"xpack.securitySolution.case.connectors.common.usernameTextFieldLabel": "ユーザー名",
- "xpack.securitySolution.case.connectors.jira.actionTypeTitle": "Jira",
- "xpack.securitySolution.case.connectors.jira.apiTokenTextFieldLabel": "APIトークンまたはパスワード",
- "xpack.securitySolution.case.connectors.jira.emailTextFieldLabel": "電子メールアドレスまたはユーザー名",
- "xpack.securitySolution.case.connectors.jira.projectKey": "プロジェクトキー",
- "xpack.securitySolution.case.connectors.jira.requiredApiTokenTextField": "APIトークンまたはパスワードが必要です",
- "xpack.securitySolution.case.connectors.jira.requiredEmailTextField": "電子メールアドレスまたはユーザー名が必要です",
- "xpack.securitySolution.case.connectors.jira.requiredProjectKeyTextField": "プロジェクトキーが必要です",
- "xpack.securitySolution.case.connectors.jira.selectMessageText": "Jiraでセキュリティケースデータを更新するか、新しいインシデントにプッシュ",
"xpack.securitySolution.case.connectors.resilient.actionTypeTitle": "IBM Resilient",
"xpack.securitySolution.case.connectors.resilient.apiKeyId": "APIキーID",
"xpack.securitySolution.case.connectors.resilient.apiKeySecret": "APIキーシークレット",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 4fa6544caf191..8841db0be8d95 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -4446,7 +4446,6 @@
"xpack.actions.builtin.case.connectorApiNullError": "需要指定连接器 [apiUrl]",
"xpack.actions.builtin.case.jiraTitle": "Jira",
"xpack.actions.builtin.case.resilientTitle": "IBM Resilient",
- "xpack.actions.builtin.configuration.apiWhitelistError": "配置连接器操作时出错:{message}",
"xpack.actions.builtin.email.errorSendingErrorMessage": "发送电子邮件时出错",
"xpack.actions.builtin.emailTitle": "电子邮件",
"xpack.actions.builtin.esIndex.errorIndexingErrorMessage": "索引文档时出错",
@@ -15123,7 +15122,6 @@
"xpack.securitySolution.case.configureCases.mappingFieldDescription": "描述",
"xpack.securitySolution.case.configureCases.mappingFieldName": "名称",
"xpack.securitySolution.case.configureCases.mappingFieldNotMapped": "未映射",
- "xpack.securitySolution.case.configureCases.mappingFieldSummary": "摘要",
"xpack.securitySolution.case.configureCases.noConnector": "未选择连接器",
"xpack.securitySolution.case.configureCases.updateConnector": "更新连接器",
"xpack.securitySolution.case.configureCases.updateSelectedConnector": "更新 { connectorName }",
@@ -15147,14 +15145,6 @@
"xpack.securitySolution.case.connectors.common.requiredPasswordTextField": "“密码”必填",
"xpack.securitySolution.case.connectors.common.requiredUsernameTextField": "“用户名”必填",
"xpack.securitySolution.case.connectors.common.usernameTextFieldLabel": "用户名",
- "xpack.securitySolution.case.connectors.jira.actionTypeTitle": "Jira",
- "xpack.securitySolution.case.connectors.jira.apiTokenTextFieldLabel": "API 令牌或密码",
- "xpack.securitySolution.case.connectors.jira.emailTextFieldLabel": "电子邮件或用户名",
- "xpack.securitySolution.case.connectors.jira.projectKey": "项目键",
- "xpack.securitySolution.case.connectors.jira.requiredApiTokenTextField": "“API 令牌或密码”必填",
- "xpack.securitySolution.case.connectors.jira.requiredEmailTextField": "“电子邮件或用户名”必填",
- "xpack.securitySolution.case.connectors.jira.requiredProjectKeyTextField": "“项目键”必填",
- "xpack.securitySolution.case.connectors.jira.selectMessageText": "将 Security 案例数据推送或更新到 Jira 中的新问题",
"xpack.securitySolution.case.connectors.resilient.actionTypeTitle": "IBM Resilient",
"xpack.securitySolution.case.connectors.resilient.apiKeyId": "API 密钥 ID",
"xpack.securitySolution.case.connectors.resilient.apiKeySecret": "API 密钥密码",
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/field_mapping.tsx
similarity index 95%
rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping.tsx
rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/field_mapping.tsx
index 52b881a1eb75f..a3382513d2bcb 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/field_mapping.tsx
@@ -13,9 +13,8 @@ import * as i18n from './translations';
import { setActionTypeToMapping, setThirdPartyToMapping } from './utils';
import { ThirdPartyField as ConnectorConfigurationThirdPartyField } from './types';
-import { CasesConfigurationMapping } from '../types';
-import { connectorConfiguration } from '../config';
-import { createDefaultMapping } from '../servicenow_connectors';
+import { CasesConfigurationMapping } from './types';
+import { createDefaultMapping } from './utils';
const FieldRowWrapper = styled.div`
margin-top: 8px;
@@ -70,15 +69,15 @@ const getThirdPartyOptions = (
export interface FieldMappingProps {
disabled: boolean;
mapping: CasesConfigurationMapping[] | null;
- connectorActionTypeId: string;
onChangeMapping: (newMapping: CasesConfigurationMapping[]) => void;
+ connectorConfiguration: Record;
}
const FieldMappingComponent: React.FC = ({
disabled,
mapping,
onChangeMapping,
- connectorActionTypeId,
+ connectorConfiguration,
}) => {
const onChangeActionType = useCallback(
(caseField: string, newActionType: string) => {
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping_row.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/field_mapping_row.tsx
similarity index 100%
rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping_row.tsx
rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/field_mapping_row.tsx
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/config.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/index.ts
similarity index 51%
rename from x-pack/plugins/actions/server/builtin_action_types/jira/config.ts
rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/index.ts
index 54f28e447010a..2de9b87ead3fe 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/config.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/index.ts
@@ -4,11 +4,7 @@
* 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: '.jira',
- name: i18n.NAME,
- minimumLicenseRequired: 'gold',
-};
+export * from './types';
+export * from './field_mapping';
+export * from './field_mapping_row';
+export * from './utils';
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/translations.ts
similarity index 100%
rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/translations.ts
rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/translations.ts
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/types.ts
similarity index 72%
rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/types.ts
rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/types.ts
index 6cd2200e1dc74..3571db39b596a 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/types.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/types.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { ActionType } from '../../../../../types';
+import { ActionType } from '../../../../types';
export { ActionType };
@@ -14,3 +14,8 @@ export interface ThirdPartyField {
defaultSourceField: string;
defaultActionType: string;
}
+export interface CasesConfigurationMapping {
+ source: string;
+ target: string;
+ actionType: string;
+}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/utils.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/utils.ts
similarity index 76%
rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/utils.ts
rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/utils.ts
index a173d90515302..b14b1b76427c6 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/utils.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/utils.ts
@@ -3,7 +3,7 @@
* 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 '../types';
+import { CasesConfigurationMapping } from './types';
export const setActionTypeToMapping = (
caseField: string,
@@ -36,3 +36,13 @@ export const setThirdPartyToMapping = (
}
return item;
});
+
+export const createDefaultMapping = (fields: Record): CasesConfigurationMapping[] =>
+ Object.keys(fields).map(
+ (key) =>
+ ({
+ source: fields[key].defaultSourceField,
+ target: key,
+ actionType: fields[key].defaultActionType,
+ } as CasesConfigurationMapping)
+ );
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx
index ecdfefa109f58..be3e8a31820c4 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx
@@ -17,6 +17,7 @@ describe('EmailParamsFields renders', () => {
subject: 'test',
message: 'test message',
};
+
const wrapper = mountWithIntl(
> {
+ return await http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, {
+ body: JSON.stringify({
+ params: { subAction: 'issueTypes', subActionParams: {} },
+ }),
+ signal,
+ });
+}
+
+export async function getFieldsByIssueType({
+ http,
+ signal,
+ connectorId,
+ id,
+}: {
+ http: HttpSetup;
+ signal: AbortSignal;
+ connectorId: string;
+ id: string;
+}): Promise> {
+ return await http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, {
+ body: JSON.stringify({
+ params: { subAction: 'fieldsByIssueType', subActionParams: { id } },
+ }),
+ signal,
+ });
+}
diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/config.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/config.ts
similarity index 87%
rename from x-pack/plugins/security_solution/public/common/lib/connectors/jira/config.ts
rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/config.ts
index e6151a54bff74..628600ee91c8e 100644
--- a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/config.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/config.ts
@@ -4,19 +4,17 @@
* 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: '.jira',
name: i18n.JIRA_TITLE,
logo,
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
- minimumLicenseRequired: 'platinum',
+ minimumLicenseRequired: 'gold',
fields: {
summary: {
label: i18n.MAPPING_FIELD_SUMMARY,
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/index.ts
new file mode 100644
index 0000000000000..a0170f9d84e9b
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { getActionType as getJiraActionType } from './jira';
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx
new file mode 100644
index 0000000000000..61923d8f78b51
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx
@@ -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 { JiraActionConnector } from './types';
+
+const ACTION_TYPE_ID = '.jira';
+let actionTypeModel: ActionTypeModel;
+
+beforeAll(() => {
+ const actionTypeRegistry = new TypeRegistry();
+ 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('jira connector validation', () => {
+ test('connector validation succeeds when connector config is valid', () => {
+ const actionConnector = {
+ secrets: {
+ email: 'email',
+ apiToken: 'apiToken',
+ },
+ id: 'test',
+ actionTypeId: '.jira',
+ name: 'jira',
+ isPreconfigured: false,
+ config: {
+ apiUrl: 'https://siem-kibana.atlassian.net',
+ projectKey: 'CK',
+ },
+ } as JiraActionConnector;
+
+ expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
+ errors: {
+ apiUrl: [],
+ email: [],
+ apiToken: [],
+ projectKey: [],
+ },
+ });
+ });
+
+ test('connector validation fails when connector config is not valid', () => {
+ const actionConnector = ({
+ secrets: {
+ email: 'user',
+ },
+ id: '.jira',
+ actionTypeId: '.jira',
+ name: 'jira',
+ config: {},
+ } as unknown) as JiraActionConnector;
+
+ expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
+ errors: {
+ apiUrl: ['URL is required.'],
+ email: [],
+ apiToken: ['API token or Password is required'],
+ projectKey: ['Project key is required'],
+ },
+ });
+ });
+});
+
+describe('jira 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.'],
+ },
+ });
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx
new file mode 100644
index 0000000000000..fd36bd6aeab0a
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx
@@ -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 { JiraActionConnector, JiraActionParams } from './types';
+import * as i18n from './translations';
+import { isValidUrl } from '../../../lib/value_validators';
+
+const validateConnector = (action: JiraActionConnector): ValidationResult => {
+ const validationResult = { errors: {} };
+ const errors = {
+ apiUrl: new Array(),
+ projectKey: new Array(),
+ email: new Array(),
+ apiToken: new Array(),
+ };
+ 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.projectKey) {
+ errors.projectKey = [...errors.projectKey, i18n.JIRA_PROJECT_KEY_REQUIRED];
+ }
+
+ if (!action.secrets.email) {
+ errors.email = [...errors.email, i18n.JIRA_EMAIL_REQUIRED];
+ }
+
+ if (!action.secrets.apiToken) {
+ errors.apiToken = [...errors.apiToken, i18n.JIRA_API_TOKEN_REQUIRED];
+ }
+
+ return validationResult;
+};
+
+export function getActionType(): ActionTypeModel {
+ return {
+ id: connectorConfiguration.id,
+ iconClass: logo,
+ selectMessage: i18n.JIRA_DESC,
+ actionTypeTitle: connectorConfiguration.name,
+ validateConnector,
+ actionConnectorFields: lazy(() => import('./jira_connectors')),
+ validateParams: (actionParams: JiraActionParams): ValidationResult => {
+ const validationResult = { errors: {} };
+ const errors = {
+ title: new Array(),
+ };
+ validationResult.errors = errors;
+ if (actionParams.subActionParams && !actionParams.subActionParams.title?.length) {
+ errors.title.push(i18n.TITLE_REQUIRED);
+ }
+ return validationResult;
+ },
+ actionParamsFields: lazy(() => import('./jira_params')),
+ };
+}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx
new file mode 100644
index 0000000000000..2cac1819d552d
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx
@@ -0,0 +1,99 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import { DocLinksStart } from 'kibana/public';
+import JiraConnectorFields from './jira_connectors';
+import { JiraActionConnector } from './types';
+
+describe('JiraActionConnectorFields renders', () => {
+ test('alerting Jira connector fields is rendered', () => {
+ const actionConnector = {
+ secrets: {
+ email: 'email',
+ apiToken: 'token',
+ },
+ id: 'test',
+ actionTypeId: '.jira',
+ isPreconfigured: false,
+ name: 'jira',
+ config: {
+ apiUrl: 'https://test/',
+ projectKey: 'CK',
+ },
+ } as JiraActionConnector;
+ const deps = {
+ docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart,
+ };
+ const wrapper = mountWithIntl(
+ {}}
+ editActionSecrets={() => {}}
+ docLinks={deps!.docLinks}
+ readOnly={false}
+ />
+ );
+
+ expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy();
+ expect(
+ wrapper.find('[data-test-subj="connector-jira-project-key-form-input"]').length > 0
+ ).toBeTruthy();
+
+ expect(
+ wrapper.find('[data-test-subj="connector-jira-email-form-input"]').length > 0
+ ).toBeTruthy();
+
+ expect(
+ wrapper.find('[data-test-subj="connector-jira-apiToken-form-input"]').length > 0
+ ).toBeTruthy();
+ });
+
+ test('case specific Jira connector fields is rendered', () => {
+ const actionConnector = {
+ secrets: {
+ email: 'email',
+ apiToken: 'token',
+ },
+ id: 'test',
+ actionTypeId: '.jira',
+ isPreconfigured: false,
+ name: 'jira',
+ config: {
+ apiUrl: 'https://test/',
+ projectKey: 'CK',
+ },
+ } as JiraActionConnector;
+ const deps = {
+ docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart,
+ };
+ const wrapper = mountWithIntl(
+ {}}
+ editActionSecrets={() => {}}
+ docLinks={deps!.docLinks}
+ readOnly={false}
+ consumer={'case'}
+ />
+ );
+ expect(wrapper.find('[data-test-subj="case-jira-mappings"]').length > 0).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy();
+ expect(
+ wrapper.find('[data-test-subj="connector-jira-project-key-form-input"]').length > 0
+ ).toBeTruthy();
+
+ expect(
+ wrapper.find('[data-test-subj="connector-jira-email-form-input"]').length > 0
+ ).toBeTruthy();
+
+ expect(
+ wrapper.find('[data-test-subj="connector-jira-apiToken-form-input"]').length > 0
+ ).toBeTruthy();
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx
new file mode 100644
index 0000000000000..2ab9843c143b9
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx
@@ -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 { CasesConfigurationMapping, FieldMapping, createDefaultMapping } from '../case_mappings';
+
+import * as i18n from './translations';
+import { JiraActionConnector } from './types';
+import { connectorConfiguration } from './config';
+
+const JiraConnectorFields: React.FC> = ({
+ action,
+ editActionSecrets,
+ editActionConfig,
+ errors,
+ consumer,
+ readOnly,
+ docLinks,
+}) => {
+ // TODO: remove incidentConfiguration later, when Case Jira will move their fields to the level of action execution
+ const { apiUrl, projectKey, incidentConfiguration, isCaseOwned } = action.config;
+ const mapping = incidentConfiguration ? incidentConfiguration.mapping : [];
+
+ const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl != null;
+
+ const { email, apiToken } = action.secrets;
+
+ const isProjectKeyInvalid: boolean = errors.projectKey.length > 0 && projectKey != null;
+ const isEmailInvalid: boolean = errors.email.length > 0 && email != null;
+ const isApiTokenInvalid: boolean = errors.apiToken.length > 0 && apiToken != 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 (
+ <>
+
+
+
+ handleOnChangeActionConfig('apiUrl', evt.target.value)}
+ onBlur={() => {
+ if (!apiUrl) {
+ editActionConfig('apiUrl', '');
+ }
+ }}
+ />
+
+
+
+
+
+
+
+ handleOnChangeActionConfig('projectKey', evt.target.value)}
+ onBlur={() => {
+ if (!projectKey) {
+ editActionConfig('projectKey', '');
+ }
+ }}
+ />
+
+
+
+
+
+
+
+ handleOnChangeSecretConfig('email', evt.target.value)}
+ onBlur={() => {
+ if (!email) {
+ editActionSecrets('email', '');
+ }
+ }}
+ />
+
+
+
+
+
+
+
+ handleOnChangeSecretConfig('apiToken', evt.target.value)}
+ onBlur={() => {
+ if (!apiToken) {
+ editActionSecrets('apiToken', '');
+ }
+ }}
+ />
+
+
+
+ {consumer === 'case' && ( // TODO: remove this block later, when Case Jira will move their fields to the level of action execution
+ <>
+
+
+
+
+
+
+ >
+ )}
+ >
+ );
+};
+
+// eslint-disable-next-line import/no-default-export
+export { JiraConnectorFields as default };
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx
new file mode 100644
index 0000000000000..26d358310741c
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx
@@ -0,0 +1,233 @@
+/*
+ * 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 JiraParamsFields from './jira_params';
+import { DocLinksStart } from 'kibana/public';
+
+import { useGetIssueTypes } from './use_get_issue_types';
+import { useGetFieldsByIssueType } from './use_get_fields_by_issue_type';
+
+jest.mock('../../../app_context', () => {
+ const post = jest.fn();
+ return {
+ useAppDependencies: jest.fn(() => ({ http: { post } })),
+ };
+});
+
+jest.mock('./use_get_issue_types');
+jest.mock('./use_get_fields_by_issue_type');
+
+const useGetIssueTypesMock = useGetIssueTypes as jest.Mock;
+const useGetFieldsByIssueTypeMock = useGetFieldsByIssueType as jest.Mock;
+
+const actionParams = {
+ subAction: 'pushToService',
+ subActionParams: {
+ title: 'sn title',
+ description: 'some description',
+ comments: [{ commentId: '1', comment: 'comment for jira' }],
+ issueType: '10006',
+ labels: ['kibana'],
+ priority: 'High',
+ savedObjectId: '123',
+ externalId: null,
+ },
+};
+const connector = {
+ secrets: {},
+ config: {},
+ id: 'test',
+ actionTypeId: '.test',
+ name: 'Test',
+ isPreconfigured: false,
+};
+
+describe('JiraParamsFields renders', () => {
+ const useGetIssueTypesResponse = {
+ isLoading: false,
+ issueTypes: [
+ {
+ id: '10006',
+ name: 'Task',
+ },
+ {
+ id: '10007',
+ name: 'Bug',
+ },
+ ],
+ };
+
+ const useGetFieldsByIssueTypeResponse = {
+ isLoading: false,
+ fields: {
+ summary: { allowedValues: [], defaultValue: {} },
+ labels: { allowedValues: [], defaultValue: {} },
+ description: { allowedValues: [], defaultValue: {} },
+ priority: {
+ allowedValues: [
+ {
+ name: 'Medium',
+ id: '3',
+ },
+ ],
+ defaultValue: { name: 'Medium', id: '3' },
+ },
+ },
+ };
+
+ beforeEach(() => {
+ useGetIssueTypesMock.mockReturnValue(useGetIssueTypesResponse);
+ useGetFieldsByIssueTypeMock.mockReturnValue(useGetFieldsByIssueTypeResponse);
+ });
+
+ test('all params fields are rendered', () => {
+ const wrapper = mountWithIntl(
+ {}}
+ index={0}
+ messageVariables={[]}
+ docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart}
+ actionConnector={connector}
+ />
+ );
+ expect(wrapper.find('[data-test-subj="issueTypeSelect"]').first().prop('value')).toStrictEqual(
+ '10006'
+ );
+ expect(wrapper.find('[data-test-subj="prioritySelect"]').first().prop('value')).toStrictEqual(
+ 'High'
+ );
+ 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="labelsComboBox"]').length > 0).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="commentsTextArea"]').length > 0).toBeTruthy();
+ });
+
+ test('it shows loading when loading issue types', () => {
+ useGetIssueTypesMock.mockReturnValue({ ...useGetIssueTypesResponse, isLoading: true });
+ const wrapper = mountWithIntl(
+ {}}
+ index={0}
+ messageVariables={[]}
+ docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart}
+ actionConnector={connector}
+ />
+ );
+
+ expect(
+ wrapper.find('[data-test-subj="issueTypeSelect"]').first().prop('isLoading')
+ ).toBeTruthy();
+ });
+
+ test('it shows loading when loading fields', () => {
+ useGetFieldsByIssueTypeMock.mockReturnValue({
+ ...useGetFieldsByIssueTypeResponse,
+ isLoading: true,
+ });
+
+ const wrapper = mountWithIntl(
+ {}}
+ index={0}
+ messageVariables={[]}
+ docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart}
+ actionConnector={connector}
+ />
+ );
+
+ expect(
+ wrapper.find('[data-test-subj="prioritySelect"]').first().prop('isLoading')
+ ).toBeTruthy();
+ expect(
+ wrapper.find('[data-test-subj="labelsComboBox"]').first().prop('isLoading')
+ ).toBeTruthy();
+ });
+
+ test('it disabled the fields when loading issue types', () => {
+ useGetIssueTypesMock.mockReturnValue({ ...useGetIssueTypesResponse, isLoading: true });
+
+ const wrapper = mountWithIntl(
+ {}}
+ index={0}
+ messageVariables={[]}
+ docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart}
+ actionConnector={connector}
+ />
+ );
+
+ expect(
+ wrapper.find('[data-test-subj="issueTypeSelect"]').first().prop('disabled')
+ ).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="prioritySelect"]').first().prop('disabled')).toBeTruthy();
+ expect(
+ wrapper.find('[data-test-subj="labelsComboBox"]').first().prop('isDisabled')
+ ).toBeTruthy();
+ });
+
+ test('it disabled the fields when loading fields', () => {
+ useGetFieldsByIssueTypeMock.mockReturnValue({
+ ...useGetFieldsByIssueTypeResponse,
+ isLoading: true,
+ });
+
+ const wrapper = mountWithIntl(
+ {}}
+ index={0}
+ messageVariables={[]}
+ docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart}
+ actionConnector={connector}
+ />
+ );
+
+ expect(
+ wrapper.find('[data-test-subj="issueTypeSelect"]').first().prop('disabled')
+ ).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="prioritySelect"]').first().prop('disabled')).toBeTruthy();
+ expect(
+ wrapper.find('[data-test-subj="labelsComboBox"]').first().prop('isDisabled')
+ ).toBeTruthy();
+ });
+
+ test('hide unsupported fields', () => {
+ useGetIssueTypesMock.mockReturnValue(useGetIssueTypesResponse);
+ useGetFieldsByIssueTypeMock.mockReturnValue({
+ ...useGetFieldsByIssueTypeResponse,
+ fields: {},
+ });
+ const wrapper = mountWithIntl(
+ {}}
+ index={0}
+ messageVariables={[]}
+ docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart}
+ actionConnector={connector}
+ />
+ );
+
+ expect(wrapper.find('[data-test-subj="issueTypeSelect"]').exists()).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="titleInput"]').exists()).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="commentsTextArea"]').exists()).toBeTruthy();
+
+ expect(wrapper.find('[data-test-subj="prioritySelect"]').exists()).toBeFalsy();
+ expect(wrapper.find('[data-test-subj="descriptionTextArea"]').exists()).toBeFalsy();
+ expect(wrapper.find('[data-test-subj="labelsComboBox"]').exists()).toBeFalsy();
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx
new file mode 100644
index 0000000000000..bde3d67ffd65f
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx
@@ -0,0 +1,319 @@
+/*
+ * 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, useMemo } from 'react';
+import { map } from 'lodash/fp';
+import { EuiFormRow, EuiComboBox, EuiSelectOption, EuiHorizontalRule } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { EuiSelect } from '@elastic/eui';
+import { EuiFlexGroup } from '@elastic/eui';
+import { EuiFlexItem } from '@elastic/eui';
+import { EuiSpacer } from '@elastic/eui';
+
+import { useAppDependencies } from '../../../app_context';
+import { ActionParamsProps } from '../../../../types';
+import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables';
+import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables';
+import { JiraActionParams } from './types';
+import { useGetIssueTypes } from './use_get_issue_types';
+import { useGetFieldsByIssueType } from './use_get_fields_by_issue_type';
+
+const JiraParamsFields: React.FunctionComponent> = ({
+ actionParams,
+ editAction,
+ index,
+ errors,
+ messageVariables,
+ actionConnector,
+}) => {
+ const { title, description, comments, issueType, priority, labels, savedObjectId } =
+ actionParams.subActionParams || {};
+
+ const [issueTypesSelectOptions, setIssueTypesSelectOptions] = useState([]);
+ const [firstLoad, setFirstLoad] = useState(false);
+ const [prioritiesSelectOptions, setPrioritiesSelectOptions] = useState([]);
+ const { http, toastNotifications } = useAppDependencies();
+
+ useEffect(() => {
+ setFirstLoad(true);
+ }, []);
+
+ const { isLoading: isLoadingIssueTypes, issueTypes } = useGetIssueTypes({
+ http,
+ toastNotifications,
+ actionConnector,
+ });
+
+ const { isLoading: isLoadingFields, fields } = useGetFieldsByIssueType({
+ http,
+ toastNotifications,
+ actionConnector,
+ issueType,
+ });
+
+ const hasLabels = useMemo(() => Object.prototype.hasOwnProperty.call(fields, 'labels'), [fields]);
+ const hasDescription = useMemo(
+ () => Object.prototype.hasOwnProperty.call(fields, 'description'),
+ [fields]
+ );
+ const hasPriority = useMemo(() => Object.prototype.hasOwnProperty.call(fields, 'priority'), [
+ fields,
+ ]);
+
+ useEffect(() => {
+ const options = issueTypes.map((type) => ({
+ value: type.id ?? '',
+ text: type.name ?? '',
+ }));
+
+ setIssueTypesSelectOptions(options);
+ }, [issueTypes]);
+
+ useEffect(() => {
+ if (issueType != null && fields != null) {
+ const priorities = fields.priority?.allowedValues ?? [];
+ const options = map(
+ (p) => ({
+ value: p.name,
+ text: p.name,
+ }),
+ priorities
+ );
+ setPrioritiesSelectOptions(options);
+ }
+ }, [fields, issueType]);
+
+ const labelOptions = useMemo(() => (labels ? labels.map((label: string) => ({ label })) : []), [
+ labels,
+ ]);
+
+ const editSubActionProperty = (key: string, value: any) => {
+ const newProps = { ...actionParams.subActionParams, [key]: value };
+ editAction('subActionParams', newProps, index);
+ };
+
+ // Reset parameters when changing connector
+ useEffect(() => {
+ if (!firstLoad) {
+ return;
+ }
+
+ setIssueTypesSelectOptions([]);
+ editAction('subActionParams', { title, comments, description: '', savedObjectId }, index);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [actionConnector]);
+
+ // Reset fields when changing connector or issue type
+ useEffect(() => {
+ if (!firstLoad) {
+ return;
+ }
+
+ setPrioritiesSelectOptions([]);
+ editAction(
+ 'subActionParams',
+ { title, issueType, comments, description: '', savedObjectId },
+ index
+ );
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [issueType, savedObjectId]);
+
+ 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,
+ actionParams.subAction,
+ index,
+ savedObjectId,
+ issueTypesSelectOptions,
+ issueType,
+ ]);
+
+ // Set default issue type
+ useEffect(() => {
+ if (!issueType && issueTypesSelectOptions.length > 0) {
+ editSubActionProperty('issueType', issueTypesSelectOptions[0].value as string);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [issueTypes, issueTypesSelectOptions]);
+
+ // Set default priority
+ useEffect(() => {
+ if (!priority && prioritiesSelectOptions.length > 0) {
+ editSubActionProperty('priority', prioritiesSelectOptions[0].value as string);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [actionConnector, issueType, prioritiesSelectOptions]);
+
+ return (
+
+ <>
+
+ {
+ editSubActionProperty('issueType', e.target.value);
+ }}
+ />
+
+
+ <>
+ {hasPriority && (
+ <>
+
+
+
+ {
+ editSubActionProperty('priority', e.target.value);
+ }}
+ />
+
+
+
+
+ >
+ )}
+ 0 && title !== undefined}
+ label={i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.jira.titleFieldLabel',
+ {
+ defaultMessage: 'Summary',
+ }
+ )}
+ >
+
+
+
+ {hasLabels && (
+ <>
+
+
+
+ {
+ const newOptions = [...labelOptions, { label: searchValue }];
+ editSubActionProperty(
+ 'labels',
+ newOptions.map((newOption) => newOption.label)
+ );
+ }}
+ onChange={(selectedOptions: Array<{ label: string }>) => {
+ editSubActionProperty(
+ 'labels',
+ selectedOptions.map((selectedOption) => selectedOption.label)
+ );
+ }}
+ onBlur={() => {
+ if (!labels) {
+ editSubActionProperty('labels', []);
+ }
+ }}
+ isClearable={true}
+ data-test-subj="labelsComboBox"
+ />
+
+
+
+
+ >
+ )}
+ {hasDescription && (
+
+ )}
+ {
+ editSubActionProperty(key, [{ commentId: '1', comment: value }]);
+ }}
+ messageVariables={messageVariables}
+ paramsProperty={'comments'}
+ inputTargetValue={comments && comments.length > 0 ? comments[0].comment : ''}
+ label={i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.jira.commentsTextAreaFieldLabel',
+ {
+ defaultMessage: 'Additional comments (optional)',
+ }
+ )}
+ errors={errors.comments as string[]}
+ />
+ >
+ >
+
+ );
+};
+
+// eslint-disable-next-line import/no-default-export
+export { JiraParamsFields as default };
diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/logo.svg b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/logo.svg
similarity index 100%
rename from x-pack/plugins/security_solution/public/common/lib/connectors/jira/logo.svg
rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/logo.svg
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts
new file mode 100644
index 0000000000000..bfcb72d1cb977
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts
@@ -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 JIRA_DESC = i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.jira.selectMessageText',
+ {
+ defaultMessage: 'Push or update data to a new issue in Jira',
+ }
+);
+
+export const JIRA_TITLE = i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.jira.actionTypeTitle',
+ {
+ defaultMessage: 'Jira',
+ }
+);
+
+export const API_URL_LABEL = i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.jira.apiUrlTextFieldLabel',
+ {
+ defaultMessage: 'URL',
+ }
+);
+
+export const API_URL_REQUIRED = i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiUrlTextField',
+ {
+ defaultMessage: 'URL is required.',
+ }
+);
+
+export const API_URL_INVALID = i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.jira.invalidApiUrlTextField',
+ {
+ defaultMessage: 'URL is invalid.',
+ }
+);
+
+export const JIRA_PROJECT_KEY_LABEL = i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.jira.projectKey',
+ {
+ defaultMessage: 'Project key',
+ }
+);
+
+export const JIRA_PROJECT_KEY_REQUIRED = i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredProjectKeyTextField',
+ {
+ defaultMessage: 'Project key is required',
+ }
+);
+
+export const JIRA_EMAIL_LABEL = i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.jira.emailTextFieldLabel',
+ {
+ defaultMessage: 'Email or Username',
+ }
+);
+
+export const JIRA_EMAIL_REQUIRED = i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredEmailTextField',
+ {
+ defaultMessage: 'Email or Username is required',
+ }
+);
+
+export const JIRA_API_TOKEN_LABEL = i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.jira.apiTokenTextFieldLabel',
+ {
+ defaultMessage: 'API token or Password',
+ }
+);
+
+export const JIRA_API_TOKEN_REQUIRED = i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiTokenTextField',
+ {
+ defaultMessage: 'API token or Password is required',
+ }
+);
+
+export const MAPPING_FIELD_SUMMARY = i18n.translate(
+ 'xpack.triggersActionsUI.case.configureCases.mappingFieldSummary',
+ {
+ defaultMessage: 'Summary',
+ }
+);
+
+export const DESCRIPTION_REQUIRED = i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredDescriptionTextField',
+ {
+ defaultMessage: 'Description is required.',
+ }
+);
+
+export const TITLE_REQUIRED = i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredTitleTextField',
+ {
+ defaultMessage: 'Title is required.',
+ }
+);
+
+export const MAPPING_FIELD_DESC = i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.jira.mappingFieldDescription',
+ {
+ defaultMessage: 'Description',
+ }
+);
+
+export const MAPPING_FIELD_COMMENTS = i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.jira.mappingFieldComments',
+ {
+ defaultMessage: 'Comments',
+ }
+);
+
+export const ISSUE_TYPES_API_ERROR = i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssueTypesMessage',
+ {
+ defaultMessage: 'Unable to get issue types',
+ }
+);
+
+export const FIELDS_API_ERROR = i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetFieldsMessage',
+ {
+ defaultMessage: 'Unable to get fields',
+ }
+);
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/types.ts
new file mode 100644
index 0000000000000..ff11199f35fea
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/types.ts
@@ -0,0 +1,44 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { CasesConfigurationMapping } from '../case_mappings';
+
+export interface JiraActionConnector {
+ config: JiraConfig;
+ secrets: JiraSecrets;
+}
+
+export interface JiraActionParams {
+ subAction: string;
+ subActionParams: {
+ savedObjectId: string;
+ title: string;
+ description: string;
+ comments: Array<{ commentId: string; comment: string }>;
+ externalId: string | null;
+ issueType: string;
+ priority: string;
+ labels: string[];
+ };
+}
+
+interface IncidentConfiguration {
+ mapping: CasesConfigurationMapping[];
+}
+
+interface JiraConfig {
+ apiUrl: string;
+ projectKey: string;
+ incidentConfiguration?: IncidentConfiguration;
+ isCaseOwned?: boolean;
+}
+
+interface JiraSecrets {
+ email: string;
+ apiToken: string;
+}
+
+// to remove
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx
new file mode 100644
index 0000000000000..08715822e5277
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx
@@ -0,0 +1,97 @@
+/*
+ * 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 { getFieldsByIssueType } from './api';
+import * as i18n from './translations';
+
+interface Fields {
+ [key: string]: {
+ allowedValues: Array<{ name: string; id: string }> | [];
+ defaultValue: { name: string; id: string } | {};
+ };
+}
+
+interface Props {
+ http: HttpSetup;
+ toastNotifications: Pick<
+ ToastsApi,
+ 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
+ >;
+ issueType: string;
+ actionConnector?: ActionConnector;
+}
+
+export interface UseGetFieldsByIssueType {
+ fields: Fields;
+ isLoading: boolean;
+}
+
+export const useGetFieldsByIssueType = ({
+ http,
+ toastNotifications,
+ actionConnector,
+ issueType,
+}: Props): UseGetFieldsByIssueType => {
+ const [isLoading, setIsLoading] = useState(true);
+ const [fields, setFields] = useState({});
+ const abortCtrl = useRef(new AbortController());
+
+ useEffect(() => {
+ let didCancel = false;
+ const fetchData = async () => {
+ if (!actionConnector || !issueType) {
+ setIsLoading(false);
+ return;
+ }
+
+ abortCtrl.current = new AbortController();
+ setIsLoading(true);
+ try {
+ const res = await getFieldsByIssueType({
+ http,
+ signal: abortCtrl.current.signal,
+ connectorId: actionConnector.id,
+ id: issueType,
+ });
+
+ if (!didCancel) {
+ setIsLoading(false);
+ setFields(res.data ?? {});
+ if (res.status && res.status === 'error') {
+ toastNotifications.addDanger({
+ title: i18n.FIELDS_API_ERROR,
+ text: `${res.serviceMessage ?? res.message}`,
+ });
+ }
+ }
+ } catch (error) {
+ if (!didCancel) {
+ toastNotifications.addDanger({
+ title: i18n.FIELDS_API_ERROR,
+ text: error.message,
+ });
+ }
+ }
+ };
+
+ abortCtrl.current.abort();
+ fetchData();
+
+ return () => {
+ didCancel = true;
+ setIsLoading(false);
+ abortCtrl.current.abort();
+ };
+ }, [http, actionConnector, issueType, toastNotifications]);
+
+ return {
+ isLoading,
+ fields,
+ };
+};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_issue_types.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_issue_types.tsx
new file mode 100644
index 0000000000000..9ebaf5882d9b9
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_issue_types.tsx
@@ -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 { getIssueTypes } from './api';
+import * as i18n from './translations';
+
+type IssueTypes = Array<{ id: string; name: string }>;
+
+interface Props {
+ http: HttpSetup;
+ toastNotifications: Pick<
+ ToastsApi,
+ 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
+ >;
+ actionConnector?: ActionConnector;
+}
+
+export interface UseGetIssueTypes {
+ issueTypes: IssueTypes;
+ isLoading: boolean;
+}
+
+export const useGetIssueTypes = ({
+ http,
+ actionConnector,
+ toastNotifications,
+}: Props): UseGetIssueTypes => {
+ const [isLoading, setIsLoading] = useState(true);
+ const [issueTypes, setIssueTypes] = useState([]);
+ 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 getIssueTypes({
+ http,
+ signal: abortCtrl.current.signal,
+ connectorId: actionConnector.id,
+ });
+
+ if (!didCancel) {
+ setIsLoading(false);
+ setIssueTypes(res.data ?? []);
+ if (res.status && res.status === 'error') {
+ toastNotifications.addDanger({
+ title: i18n.ISSUE_TYPES_API_ERROR,
+ text: `${res.serviceMessage ?? res.message}`,
+ });
+ }
+ }
+ } catch (error) {
+ if (!didCancel) {
+ toastNotifications.addDanger({
+ title: i18n.ISSUE_TYPES_API_ERROR,
+ text: error.message,
+ });
+ }
+ }
+ };
+
+ abortCtrl.current.abort();
+ fetchData();
+
+ return () => {
+ didCancel = true;
+ setIsLoading(false);
+ abortCtrl.current.abort();
+ };
+ }, [http, actionConnector, toastNotifications]);
+
+ return {
+ issueTypes,
+ isLoading,
+ };
+};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx
index f99a276305d75..a8f1ed8d55447 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx
@@ -18,10 +18,11 @@ import {
import { isEmpty } from 'lodash';
import { FormattedMessage } from '@kbn/i18n/react';
import { ActionConnectorFieldsProps } from '../../../../types';
+import { CasesConfigurationMapping, FieldMapping, createDefaultMapping } from '../case_mappings';
+
import * as i18n from './translations';
-import { ServiceNowActionConnector, CasesConfigurationMapping } from './types';
+import { ServiceNowActionConnector } from './types';
import { connectorConfiguration } from './config';
-import { FieldMapping } from './case_mappings/field_mapping';
const ServiceNowConnectorFields: React.FC
@@ -184,15 +185,5 @@ const ServiceNowConnectorFields: React.FC): CasesConfigurationMapping[] =>
- Object.keys(fields).map(
- (key) =>
- ({
- source: fields[key].defaultSourceField,
- target: key,
- actionType: fields[key].defaultActionType,
- } as CasesConfigurationMapping)
- );
-
// eslint-disable-next-line import/no-default-export
export { ServiceNowConnectorFields as default };
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.tsx
index 2a29018d83ff4..2a2efdfbe35b1 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.tsx
@@ -79,7 +79,14 @@ const ServiceNowParamsFields: React.FunctionComponent
- Incident
+
+ {i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.title',
+ {
+ defaultMessage: 'Incident',
+ }
+ )}
+
) : null}
diff --git a/x-pack/plugins/triggers_actions_ui/public/common/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/index.ts
index 9dd3fd787f860..8b728b5e178b5 100644
--- a/x-pack/plugins/triggers_actions_ui/public/common/index.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/common/index.ts
@@ -7,3 +7,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';
diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts
index 762f41ba3691c..109d473c56e66 100644
--- a/x-pack/plugins/triggers_actions_ui/public/types.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/types.ts
@@ -54,6 +54,7 @@ export interface ActionParamsProps {
messageVariables?: ActionVariable[];
defaultMessage?: string;
docLinks: DocLinksStart;
+ actionConnector?: ActionConnector;
}
export interface Pagination {
diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/jira.ts
new file mode 100644
index 0000000000000..025fd558ee1ca
--- /dev/null
+++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/jira.ts
@@ -0,0 +1,97 @@
+/*
+ * 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';
+
+// node ../scripts/functional_test_runner.js --grep "Actions.servicenddd" --config=test/alerting_api_integration/security_and_spaces/config.ts
+
+const mapping = [
+ {
+ source: 'title',
+ target: 'summary',
+ actionType: 'nothing',
+ },
+ {
+ source: 'description',
+ target: 'description',
+ actionType: 'nothing',
+ },
+ {
+ source: 'comments',
+ target: 'comments',
+ actionType: 'nothing',
+ },
+];
+
+// eslint-disable-next-line import/no-default-export
+export default function jiraTest({ getService }: FtrProviderContext) {
+ const supertest = getService('supertest');
+ const kibanaServer = getService('kibanaServer');
+ const mockJira = {
+ config: {
+ apiUrl: 'www.jiraisinkibanaactions.com',
+ incidentConfiguration: { mapping: [...mapping] },
+ isCaseOwned: true,
+ },
+ secrets: {
+ email: 'elastic',
+ apiToken: 'changeme',
+ },
+ params: {
+ savedObjectId: '123',
+ title: 'a title',
+ description: 'a description',
+ labels: ['kibana'],
+ issueType: '10006',
+ priority: 'High',
+ externalId: null,
+ comments: [
+ {
+ commentId: '456',
+ comment: 'first comment',
+ },
+ ],
+ },
+ };
+ describe('jira', () => {
+ let jiraSimulatorURL: string = '';
+
+ // need to wait for kibanaServer to settle ...
+ before(() => {
+ jiraSimulatorURL = kibanaServer.resolveUrl(
+ getExternalServiceSimulatorPath(ExternalServiceSimulator.JIRA)
+ );
+ });
+
+ it('should return 403 when creating a jira action', async () => {
+ await supertest
+ .post('/api/actions/action')
+ .set('kbn-xsrf', 'foo')
+ .send({
+ name: 'A jira action',
+ actionTypeId: '.jira',
+ config: {
+ apiUrl: jiraSimulatorURL,
+ projectKey: 'CK',
+ incidentConfiguration: { ...mockJira.config.incidentConfiguration },
+ isCaseOwned: true,
+ },
+ secrets: mockJira.secrets,
+ })
+ .expect(403, {
+ statusCode: 403,
+ error: 'Forbidden',
+ message:
+ 'Action type .jira is disabled because your basic license does not support it. Please upgrade your license.',
+ });
+ });
+ });
+}
diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts
index 1788a12afebf2..8f31e7f96b562 100644
--- a/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts
+++ b/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts
@@ -11,6 +11,7 @@ export default function actionsTests({ loadTestFile }: FtrProviderContext) {
describe('Actions', () => {
loadTestFile(require.resolve('./builtin_action_types/email'));
loadTestFile(require.resolve('./builtin_action_types/es_index'));
+ loadTestFile(require.resolve('./builtin_action_types/jira'));
loadTestFile(require.resolve('./builtin_action_types/pagerduty'));
loadTestFile(require.resolve('./builtin_action_types/server_log'));
loadTestFile(require.resolve('./builtin_action_types/servicenow'));
diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/jira_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/jira_simulation.ts
index 4b65b7a8f2636..6041251dc28a4 100644
--- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/jira_simulation.ts
+++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/jira_simulation.ts
@@ -105,6 +105,57 @@ export function initPlugin(router: IRouter, path: string) {
});
}
);
+
+ router.get(
+ {
+ path: `${path}/rest/capabilities`,
+ options: {
+ authRequired: false,
+ },
+ validate: {},
+ },
+ async function (
+ context: RequestHandlerContext,
+ req: KibanaRequest,
+ res: KibanaResponseFactory
+ ): Promise> {
+ return jsonResponse(res, 200, {
+ capabilities: {},
+ });
+ }
+ );
+
+ router.get(
+ {
+ path: `${path}/rest/api/2/issue/createmeta`,
+ options: {
+ authRequired: false,
+ },
+ validate: {},
+ },
+ async function (
+ context: RequestHandlerContext,
+ req: KibanaRequest,
+ res: KibanaResponseFactory
+ ): Promise> {
+ return jsonResponse(res, 200, {
+ projects: [
+ {
+ issuetypes: [
+ {
+ id: '10006',
+ name: 'Task',
+ },
+ {
+ id: '10007',
+ name: 'Sub-task',
+ },
+ ],
+ },
+ ],
+ });
+ }
+ );
}
function jsonResponse(
diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts
index 0f7acf5ead1a1..88f0f02794c9b 100644
--- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts
+++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts
@@ -38,6 +38,7 @@ export function getAllExternalServiceSimulatorPaths(): string[] {
);
allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v2/table/incident`);
allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.JIRA}/rest/api/2/issue`);
+ allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.JIRA}/rest/api/2/createmeta`);
allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.RESILIENT}/rest/orgs/201/incidents`);
return allPaths;
}
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts
index 3ffd58b945ddb..84fad699525a9 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts
@@ -43,7 +43,7 @@ export default function jiraTest({ getService }: FtrProviderContext) {
config: {
apiUrl: 'www.jiraisinkibanaactions.com',
projectKey: 'CK',
- casesConfiguration: { mapping },
+ incidentConfiguration: { mapping },
},
secrets: {
apiToken: 'elastic',
@@ -94,6 +94,8 @@ export default function jiraTest({ getService }: FtrProviderContext) {
config: {
...mockJira.config,
apiUrl: jiraSimulatorURL,
+ incidentConfiguration: mockJira.config.incidentConfiguration,
+ isCaseOwned: true,
},
secrets: mockJira.secrets,
})
@@ -107,7 +109,8 @@ export default function jiraTest({ getService }: FtrProviderContext) {
config: {
apiUrl: jiraSimulatorURL,
projectKey: mockJira.config.projectKey,
- casesConfiguration: mockJira.config.casesConfiguration,
+ incidentConfiguration: mockJira.config.incidentConfiguration,
+ isCaseOwned: true,
},
});
@@ -123,7 +126,8 @@ export default function jiraTest({ getService }: FtrProviderContext) {
config: {
apiUrl: jiraSimulatorURL,
projectKey: mockJira.config.projectKey,
- casesConfiguration: mockJira.config.casesConfiguration,
+ incidentConfiguration: mockJira.config.incidentConfiguration,
+ isCaseOwned: true,
},
});
});
@@ -178,7 +182,7 @@ export default function jiraTest({ getService }: FtrProviderContext) {
config: {
apiUrl: 'http://jira.mynonexistent.com',
projectKey: mockJira.config.projectKey,
- casesConfiguration: mockJira.config.casesConfiguration,
+ incidentConfiguration: mockJira.config.incidentConfiguration,
},
secrets: mockJira.secrets,
})
@@ -203,7 +207,7 @@ export default function jiraTest({ getService }: FtrProviderContext) {
config: {
apiUrl: jiraSimulatorURL,
projectKey: mockJira.config.projectKey,
- casesConfiguration: mockJira.config.casesConfiguration,
+ incidentConfiguration: mockJira.config.incidentConfiguration,
},
})
.expect(400)
@@ -217,30 +221,6 @@ export default function jiraTest({ getService }: FtrProviderContext) {
});
});
- it('should respond with a 400 Bad Request when creating a jira action without casesConfiguration', async () => {
- await supertest
- .post('/api/actions/action')
- .set('kbn-xsrf', 'foo')
- .send({
- name: 'A jira action',
- actionTypeId: '.jira',
- config: {
- apiUrl: jiraSimulatorURL,
- projectKey: mockJira.config.projectKey,
- },
- secrets: mockJira.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 jira action with empty mapping', async () => {
await supertest
.post('/api/actions/action')
@@ -251,7 +231,7 @@ export default function jiraTest({ getService }: FtrProviderContext) {
config: {
apiUrl: jiraSimulatorURL,
projectKey: mockJira.config.projectKey,
- casesConfiguration: { mapping: [] },
+ incidentConfiguration: { mapping: [] },
},
secrets: mockJira.secrets,
})
@@ -261,7 +241,7 @@ export default function jiraTest({ 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',
});
});
});
@@ -276,7 +256,7 @@ export default function jiraTest({ getService }: FtrProviderContext) {
config: {
apiUrl: jiraSimulatorURL,
projectKey: mockJira.config.projectKey,
- casesConfiguration: {
+ incidentConfiguration: {
mapping: [
{
source: 'title',
@@ -307,7 +287,7 @@ export default function jiraTest({ getService }: FtrProviderContext) {
config: {
apiUrl: jiraSimulatorURL,
projectKey: mockJira.config.projectKey,
- casesConfiguration: mockJira.config.casesConfiguration,
+ incidentConfiguration: mockJira.config.incidentConfiguration,
},
secrets: mockJira.secrets,
});
@@ -353,7 +333,7 @@ export default function jiraTest({ 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 [issueTypes]\n- [4.subAction]: expected value to equal [fieldsByIssueType]',
});
});
});
@@ -371,7 +351,7 @@ export default function jiraTest({ 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 [issueTypes]\n- [4.subAction]: expected value to equal [fieldsByIssueType]',
});
});
});
@@ -389,7 +369,7 @@ export default function jiraTest({ 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 [issueTypes]\n- [4.subAction]: expected value to equal [fieldsByIssueType]',
});
});
});
@@ -412,31 +392,7 @@ export default function jiraTest({ 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: {
- ...mockJira.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 [issueTypes]\n- [4.subAction]: expected value to equal [fieldsByIssueType]',
});
});
});
@@ -464,7 +420,7 @@ export default function jiraTest({ 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 [issueTypes]\n- [4.subAction]: expected value to equal [fieldsByIssueType]',
});
});
});
@@ -492,35 +448,7 @@ export default function jiraTest({ 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: {
- ...mockJira.params,
- subActionParams: {
- ...mockJira.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 [issueTypes]\n- [4.subAction]: expected value to equal [fieldsByIssueType]',
});
});
});
@@ -537,6 +465,7 @@ export default function jiraTest({ getService }: FtrProviderContext) {
subActionParams: {
...mockJira.params.subActionParams,
comments: [],
+ issueType: '10006',
},
},
})
diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts
index fb6f4fce3c29a..c23df53c4feef 100644
--- a/x-pack/test/case_api_integration/common/lib/utils.ts
+++ b/x-pack/test/case_api_integration/common/lib/utils.ts
@@ -66,7 +66,7 @@ export const getJiraConnector = () => ({
config: {
apiUrl: 'http://some.non.existent.com',
projectKey: 'pkey',
- casesConfiguration: {
+ incidentConfiguration: {
mapping: [
{
source: 'title',
@@ -85,6 +85,7 @@ export const getJiraConnector = () => ({
},
],
},
+ isCaseOwned: true,
},
});
From bd384822f22dbb3b17699d22305bd31c1426f075 Mon Sep 17 00:00:00 2001
From: Nick Partridge
Date: Wed, 9 Sep 2020 08:26:39 -0500
Subject: [PATCH 06/39] Upgrade elastic charts to 21.1.2 (#76939)
---
package.json | 2 +-
packages/kbn-ui-shared-deps/package.json | 2 +-
yarn.lock | 31 +++---------------------
3 files changed, 6 insertions(+), 29 deletions(-)
diff --git a/package.json b/package.json
index ff487510f7a32..95a6de337f62a 100644
--- a/package.json
+++ b/package.json
@@ -231,7 +231,7 @@
"@babel/parser": "^7.11.2",
"@babel/types": "^7.11.0",
"@elastic/apm-rum": "^5.5.0",
- "@elastic/charts": "21.0.1",
+ "@elastic/charts": "21.1.2",
"@elastic/ems-client": "7.9.3",
"@elastic/eslint-config-kibana": "0.15.0",
"@elastic/eslint-plugin-eui": "0.0.2",
diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json
index 4b2e88d155245..bbe7b1bc2e8da 100644
--- a/packages/kbn-ui-shared-deps/package.json
+++ b/packages/kbn-ui-shared-deps/package.json
@@ -9,7 +9,7 @@
"kbn:watch": "node scripts/build --dev --watch"
},
"dependencies": {
- "@elastic/charts": "21.0.1",
+ "@elastic/charts": "21.1.2",
"@elastic/eui": "28.2.0",
"@elastic/numeral": "^2.5.0",
"@kbn/i18n": "1.0.0",
diff --git a/yarn.lock b/yarn.lock
index bb3f8baea9692..ddb83b3cf1532 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1144,10 +1144,10 @@
dependencies:
"@elastic/apm-rum-core" "^5.6.0"
-"@elastic/charts@21.0.1":
- version "21.0.1"
- resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-21.0.1.tgz#1e8be5303d0c7d53d7ccfcfa121ef68164ae200f"
- integrity sha512-KMJE2JNwoy021Rvhgu1wiga0FVCa0u4NlFVUSR+h+G010KLarc+c9yUKBTG8v/nZ6ijBtuOLCjjU9OCWXYfxvA==
+"@elastic/charts@21.1.2":
+ version "21.1.2"
+ resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-21.1.2.tgz#da7e9c1025bf730a738b6ac6d7024d97dd2b5aa2"
+ integrity sha512-Uri+Xolgii7/mRSarfXTfA6X2JC76ILIxTPO8RlYdI44gzprJfUO7Aw5s8vVQke3x6Cu39a+9B0s6TY4GAaApQ==
dependencies:
"@popperjs/core" "^2.4.0"
chroma-js "^2.1.0"
@@ -1168,9 +1168,6 @@
ts-debounce "^1.0.0"
utility-types "^3.10.0"
uuid "^3.3.2"
- optionalDependencies:
- redux-immutable-state-invariant "^2.1.0"
- redux-logger "^3.0.6"
"@elastic/elasticsearch@7.9.0-rc.1":
version "7.9.0-rc.1"
@@ -10394,11 +10391,6 @@ dedent@^0.7.0:
resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=
-deep-diff@^0.3.5:
- version "0.3.8"
- resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-0.3.8.tgz#c01de63efb0eec9798801d40c7e0dae25b582c84"
- integrity sha1-wB3mPvsO7JeYgB1Ax+Da4ltYLIQ=
-
deep-eql@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-0.1.3.tgz#ef558acab8de25206cd713906d74e56930eb69f2"
@@ -24450,21 +24442,6 @@ redux-devtools-extension@^2.13.8:
resolved "https://registry.yarnpkg.com/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz#37b982688626e5e4993ff87220c9bbb7cd2d96e1"
integrity sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg==
-redux-immutable-state-invariant@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/redux-immutable-state-invariant/-/redux-immutable-state-invariant-2.1.0.tgz#308fd3cc7415a0e7f11f51ec997b6379c7055ce1"
- integrity sha512-3czbDKs35FwiBRsx/3KabUk5zSOoTXC+cgVofGkpBNv3jQcqIe5JrHcF5AmVt7B/4hyJ8MijBIpCJ8cife6yJg==
- dependencies:
- invariant "^2.1.0"
- json-stringify-safe "^5.0.1"
-
-redux-logger@^3.0.6:
- version "3.0.6"
- resolved "https://registry.yarnpkg.com/redux-logger/-/redux-logger-3.0.6.tgz#f7555966f3098f3c88604c449cf0baf5778274bf"
- integrity sha1-91VZZvMJjzyIYExEnPC69XeCdL8=
- dependencies:
- deep-diff "^0.3.5"
-
redux-observable@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/redux-observable/-/redux-observable-1.2.0.tgz#ff51b6c6be2598e9b5e89fc36639186bb0e669c7"
From bfab38020aa77ac600c1383f00b844ccf4b7f9d5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?=
Date: Wed, 9 Sep 2020 15:30:38 +0200
Subject: [PATCH 07/39] [Security Solution] Refactor Network Details to use
Search Strategy (#76928)
Co-authored-by: Elastic Machine
---
.../security_solution/index.ts | 12 +
.../security_solution/network/common/index.ts | 7 +
.../network/details/index.ts | 109 +++++++
.../security_solution/network/http/index.ts | 10 +
.../security_solution/network/index.ts | 4 +
.../security_solution/network/tls/index.ts | 14 +-
.../network/top_countries/index.ts | 7 -
.../security_solution/network/users/index.ts | 73 +++++
.../components/link_to/__mocks__/index.ts | 2 +-
.../public/common/components/link_to/index.ts | 2 +-
.../link_to/redirect_to_network.tsx | 2 +-
.../common/components/links/index.test.tsx | 10 +-
.../public/common/components/links/index.tsx | 10 +-
.../common/components/links/translations.ts | 2 +-
.../get_anomalies_network_table_columns.tsx | 4 +-
.../navigation/breadcrumbs/index.ts | 2 +-
.../components/paginated_table/index.tsx | 10 +-
.../public/common/mock/global_state.ts | 26 +-
.../authentications_table/index.tsx | 6 +-
.../public/hosts/pages/details/index.tsx | 3 +-
.../__snapshots__/index.test.tsx.snap | 0
.../{ip_overview => details}/index.test.tsx | 0
.../{ip_overview => details}/index.tsx | 31 +-
.../{ip_overview => details}/mock.ts | 11 +-
.../{ip_overview => details}/translations.ts | 0
.../point_tool_tip_content.test.tsx | 10 +-
.../map_tool_tip/point_tool_tip_content.tsx | 4 +-
.../flow_target_select_connected/index.tsx | 6 +-
.../__snapshots__/index.test.tsx.snap | 101 +-----
.../components/network_http_table/columns.tsx | 10 +-
.../components/network_http_table/index.tsx | 84 +++--
.../network_top_countries_table/columns.tsx | 4 +-
.../network_top_countries_table/index.tsx | 4 +-
.../network_top_n_flow_table/columns.tsx | 8 +-
.../network_top_n_flow_table/index.tsx | 4 +-
.../__snapshots__/index.test.tsx.snap | 78 +----
.../network/components/tls_table/index.tsx | 201 ++++++------
.../__snapshots__/index.test.tsx.snap | 82 +----
.../network/components/users_table/index.tsx | 211 ++++++-------
.../index.gql_query.ts | 0
.../network/containers/details/index.tsx | 153 +++++++++
.../containers/details/translations.ts | 21 ++
.../network/containers/ip_overview/index.tsx | 85 -----
.../network/containers/network_dns/index.tsx | 6 +-
.../public/network/containers/users/index.tsx | 291 ++++++++++--------
.../network/containers/users/translations.ts | 21 ++
.../{ip_details => details}/index.test.tsx | 77 ++---
.../pages/{ip_details => details}/index.tsx | 156 +++++-----
.../network_http_query_table.tsx | 0
.../network_top_countries_query_table.tsx | 0
.../network_top_n_flow_query_table.tsx | 0
.../tls_query_table.tsx | 0
.../pages/{ip_details => details}/types.ts | 5 -
.../pages/details/users_query_table.tsx | 57 ++++
.../pages/{ip_details => details}/utils.ts | 4 +-
.../public/network/pages/index.tsx | 13 +-
.../__snapshots__/index.test.tsx.snap | 18 --
.../pages/ip_details/users_query_table.tsx | 56 ----
.../public/network/store/actions.ts | 4 +-
.../public/network/store/helpers.test.ts | 42 +--
.../public/network/store/helpers.ts | 36 +--
.../public/network/store/model.ts | 73 ++---
.../public/network/store/reducer.ts | 32 +-
.../public/network/store/selectors.ts | 11 +-
.../components/host_overview/index.tsx | 6 +-
.../overview/components/host_overview/mock.ts | 5 +-
.../field_renderers/field_renderers.test.tsx | 27 +-
.../field_renderers/field_renderers.tsx | 25 +-
.../components/formatted_ip/index.tsx | 4 +-
.../factory/network/details/helpers.ts | 55 ++++
.../factory/network/details/index.ts | 46 +++
.../details/query.details_network.dsl.ts | 126 ++++++++
.../factory/network/index.ts | 4 +
.../factory/network/tls/helpers.ts | 14 +-
.../factory/network/tls/index.ts | 15 +-
.../network/tls/query.tls_network.dsl.ts | 12 +-
.../factory/network/users/helpers.ts | 36 +++
.../factory/network/users/index.ts | 57 ++++
.../network/users/query.users_network.dsl.ts | 104 +++++++
.../{ip_overview.ts => network_details.ts} | 2 +-
80 files changed, 1664 insertions(+), 1199 deletions(-)
create mode 100644 x-pack/plugins/security_solution/common/search_strategy/security_solution/network/details/index.ts
create mode 100644 x-pack/plugins/security_solution/common/search_strategy/security_solution/network/users/index.ts
rename x-pack/plugins/security_solution/public/network/components/{ip_overview => details}/__snapshots__/index.test.tsx.snap (100%)
rename x-pack/plugins/security_solution/public/network/components/{ip_overview => details}/index.test.tsx (100%)
rename x-pack/plugins/security_solution/public/network/components/{ip_overview => details}/index.tsx (88%)
rename x-pack/plugins/security_solution/public/network/components/{ip_overview => details}/mock.ts (85%)
rename x-pack/plugins/security_solution/public/network/components/{ip_overview => details}/translations.ts (100%)
rename x-pack/plugins/security_solution/public/network/containers/{ip_overview => details}/index.gql_query.ts (100%)
create mode 100644 x-pack/plugins/security_solution/public/network/containers/details/index.tsx
create mode 100644 x-pack/plugins/security_solution/public/network/containers/details/translations.ts
delete mode 100644 x-pack/plugins/security_solution/public/network/containers/ip_overview/index.tsx
create mode 100644 x-pack/plugins/security_solution/public/network/containers/users/translations.ts
rename x-pack/plugins/security_solution/public/network/pages/{ip_details => details}/index.test.tsx (67%)
rename x-pack/plugins/security_solution/public/network/pages/{ip_details => details}/index.tsx (69%)
rename x-pack/plugins/security_solution/public/network/pages/{ip_details => details}/network_http_query_table.tsx (100%)
rename x-pack/plugins/security_solution/public/network/pages/{ip_details => details}/network_top_countries_query_table.tsx (100%)
rename x-pack/plugins/security_solution/public/network/pages/{ip_details => details}/network_top_n_flow_query_table.tsx (100%)
rename x-pack/plugins/security_solution/public/network/pages/{ip_details => details}/tls_query_table.tsx (100%)
rename x-pack/plugins/security_solution/public/network/pages/{ip_details => details}/types.ts (92%)
create mode 100644 x-pack/plugins/security_solution/public/network/pages/details/users_query_table.tsx
rename x-pack/plugins/security_solution/public/network/pages/{ip_details => details}/utils.ts (94%)
delete mode 100644 x-pack/plugins/security_solution/public/network/pages/ip_details/__snapshots__/index.test.tsx.snap
delete mode 100644 x-pack/plugins/security_solution/public/network/pages/ip_details/users_query_table.tsx
create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/helpers.ts
create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/index.ts
create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/query.details_network.dsl.ts
create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/users/helpers.ts
create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/users/index.ts
create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/users/query.users_network.dsl.ts
rename x-pack/test/api_integration/apis/security_solution/{ip_overview.ts => network_details.ts} (97%)
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts
index b7d905d22e839..35fcc3b07fd05 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts
@@ -23,6 +23,8 @@ import {
} from './hosts';
import {
NetworkQueries,
+ NetworkDetailsStrategyResponse,
+ NetworkDetailsRequestOptions,
NetworkDnsStrategyResponse,
NetworkDnsRequestOptions,
NetworkTlsStrategyResponse,
@@ -35,6 +37,8 @@ import {
NetworkTopCountriesRequestOptions,
NetworkTopNFlowStrategyResponse,
NetworkTopNFlowRequestOptions,
+ NetworkUsersStrategyResponse,
+ NetworkUsersRequestOptions,
} from './network';
import {
MatrixHistogramQuery,
@@ -87,6 +91,8 @@ export type StrategyResponseType = T extends HostsQ
? HostFirstLastSeenStrategyResponse
: T extends HostsQueries.uncommonProcesses
? HostUncommonProcessesStrategyResponse
+ : T extends NetworkQueries.details
+ ? NetworkDetailsStrategyResponse
: T extends NetworkQueries.dns
? NetworkDnsStrategyResponse
: T extends NetworkQueries.http
@@ -99,6 +105,8 @@ export type StrategyResponseType = T extends HostsQ
? NetworkTopCountriesStrategyResponse
: T extends NetworkQueries.topNFlow
? NetworkTopNFlowStrategyResponse
+ : T extends NetworkQueries.users
+ ? NetworkUsersStrategyResponse
: T extends typeof MatrixHistogramQuery
? MatrixHistogramStrategyResponse
: never;
@@ -115,6 +123,8 @@ export type StrategyRequestType = T extends HostsQu
? HostFirstLastSeenRequestOptions
: T extends HostsQueries.uncommonProcesses
? HostUncommonProcessesRequestOptions
+ : T extends NetworkQueries.details
+ ? NetworkDetailsRequestOptions
: T extends NetworkQueries.dns
? NetworkDnsRequestOptions
: T extends NetworkQueries.http
@@ -127,6 +137,8 @@ export type StrategyRequestType = T extends HostsQu
? NetworkTopCountriesRequestOptions
: T extends NetworkQueries.topNFlow
? NetworkTopNFlowRequestOptions
+ : T extends NetworkQueries.users
+ ? NetworkUsersRequestOptions
: T extends typeof MatrixHistogramQuery
? MatrixHistogramRequestOptions
: never;
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common/index.ts
index 66676569b3c9e..19521741c5f66 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common/index.ts
@@ -15,6 +15,13 @@ export enum NetworkTopTablesFields {
source_ips = 'source_ips',
}
+export enum FlowTarget {
+ client = 'client',
+ destination = 'destination',
+ server = 'server',
+ source = 'source',
+}
+
export enum FlowTargetSourceDest {
destination = 'destination',
source = 'source',
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/details/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/details/index.ts
new file mode 100644
index 0000000000000..920d7cf8c5eaf
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/details/index.ts
@@ -0,0 +1,109 @@
+/*
+ * 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 { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common';
+import { HostEcs } from '../../../../ecs/host';
+import { GeoEcs } from '../../../../ecs/geo';
+import { Inspect, Maybe, TotalValue, Hit, ShardsResponse } from '../../../common';
+import { RequestBasicOptions } from '../..';
+
+export interface NetworkDetailsRequestOptions extends Omit {
+ ip: string;
+}
+
+export interface NetworkDetailsStrategyResponse extends IEsSearchResponse {
+ networkDetails: {
+ client?: Maybe;
+ destination?: Maybe;
+ host?: HostEcs;
+ server?: Maybe;
+ source?: Maybe;
+ };
+ inspect?: Maybe;
+}
+
+export interface NetworkDetails {
+ firstSeen?: Maybe;
+ lastSeen?: Maybe;
+ autonomousSystem: AutonomousSystem;
+ geo: GeoEcs;
+}
+
+export interface AutonomousSystem {
+ number?: Maybe;
+ organization?: Maybe;
+}
+
+export interface AutonomousSystemOrganization {
+ name?: Maybe;
+}
+
+interface ResultHit {
+ doc_count: number;
+ results: {
+ hits: {
+ total: TotalValue | number;
+ max_score: number | null;
+ hits: Array<{
+ _source: T;
+ sort?: [number];
+ _index?: string;
+ _type?: string;
+ _id?: string;
+ _score?: number | null;
+ }>;
+ };
+ };
+}
+
+export interface NetworkHit {
+ took?: number;
+ timed_out?: boolean;
+ _scroll_id?: string;
+ _shards?: ShardsResponse;
+ timeout?: number;
+ hits?: {
+ total: number;
+ hits: Hit[];
+ };
+ doc_count: number;
+ geo: ResultHit