diff --git a/x-pack/plugins/case/common/api/connectors/mappings.ts b/x-pack/plugins/case/common/api/connectors/mappings.ts index 3e8baf0af28343..b91f9d69e85e26 100644 --- a/x-pack/plugins/case/common/api/connectors/mappings.ts +++ b/x-pack/plugins/case/common/api/connectors/mappings.ts @@ -180,6 +180,8 @@ export const PostPushRequestRt = rt.type({ params: ServiceConnectorCaseParamsRt, }); +export type PostPushRequest = rt.TypeOf; + export interface SimpleComment { comment: string; commentId: string; diff --git a/x-pack/plugins/case/server/client/configure/get_fields.test.ts b/x-pack/plugins/case/server/client/configure/get_fields.test.ts new file mode 100644 index 00000000000000..b465d916b2292e --- /dev/null +++ b/x-pack/plugins/case/server/client/configure/get_fields.test.ts @@ -0,0 +1,60 @@ +/* + * 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 { ConnectorTypes } from '../../../common/api'; + +import { createMockSavedObjectsRepository, mockCaseMappings } from '../../routes/api/__fixtures__'; +import { createCaseClientWithMockSavedObjectsClient } from '../mocks'; +import { actionsClientMock } from '../../../../actions/server/actions_client.mock'; +import { actionsErrResponse, mappings, mockGetFieldsResponse } from './mock'; +describe('get_fields', () => { + const execute = jest.fn().mockReturnValue(mockGetFieldsResponse); + const actionsMock = { ...actionsClientMock.create(), execute }; + beforeEach(async () => { + jest.clearAllMocks(); + }); + + describe('happy path', () => { + test('it gets fields', async () => { + const savedObjectsClient = createMockSavedObjectsRepository({ + caseMappingsSavedObject: mockCaseMappings, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const res = await caseClient.client.getFields({ + actionsClient: actionsMock, + connectorType: ConnectorTypes.jira, + connectorId: '123', + }); + expect(res).toEqual({ + fields: [ + { id: 'summary', name: 'Summary', required: true, type: 'text' }, + { id: 'description', name: 'Description', required: false, type: 'text' }, + ], + defaultMappings: mappings[ConnectorTypes.jira], + }); + }); + }); + + describe('unhappy path', () => { + test('it throws error', async () => { + const savedObjectsClient = createMockSavedObjectsRepository({ + caseMappingsSavedObject: mockCaseMappings, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + await caseClient.client + .getFields({ + actionsClient: { ...actionsMock, execute: jest.fn().mockReturnValue(actionsErrResponse) }, + connectorType: ConnectorTypes.jira, + connectorId: '123', + }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(424); + }); + }); + }); +}); diff --git a/x-pack/plugins/case/server/client/configure/get_mappings.test.ts b/x-pack/plugins/case/server/client/configure/get_mappings.test.ts new file mode 100644 index 00000000000000..e68db5cde940bb --- /dev/null +++ b/x-pack/plugins/case/server/client/configure/get_mappings.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ConnectorTypes } from '../../../common/api'; + +import { createMockSavedObjectsRepository, mockCaseMappings } from '../../routes/api/__fixtures__'; +import { createCaseClientWithMockSavedObjectsClient } from '../mocks'; +import { actionsClientMock } from '../../../../actions/server/actions_client.mock'; +import { mappings, mockGetFieldsResponse } from './mock'; + +describe('get_mappings', () => { + const execute = jest.fn().mockReturnValue(mockGetFieldsResponse); + const actionsMock = { ...actionsClientMock.create(), execute }; + beforeEach(async () => { + jest.restoreAllMocks(); + const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; + spyOnDate.mockImplementation(() => ({ + toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'), + })); + }); + + describe('happy path', () => { + test('it gets existing mappings', async () => { + const savedObjectsClient = createMockSavedObjectsRepository({ + caseMappingsSavedObject: mockCaseMappings, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const res = await caseClient.client.getMappings({ + actionsClient: actionsMock, + caseClient: caseClient.client, + connectorType: ConnectorTypes.jira, + connectorId: '123', + }); + + expect(res).toEqual(mappings[ConnectorTypes.jira]); + }); + test('it creates new mappings', async () => { + const savedObjectsClient = createMockSavedObjectsRepository({ + caseMappingsSavedObject: [], + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const res = await caseClient.client.getMappings({ + actionsClient: actionsMock, + caseClient: caseClient.client, + connectorType: ConnectorTypes.jira, + connectorId: '123', + }); + + expect(res).toEqual(mappings[ConnectorTypes.jira]); + }); + }); +}); diff --git a/x-pack/plugins/case/server/client/configure/mock.ts b/x-pack/plugins/case/server/client/configure/mock.ts new file mode 100644 index 00000000000000..bb57755250ba2c --- /dev/null +++ b/x-pack/plugins/case/server/client/configure/mock.ts @@ -0,0 +1,625 @@ +/* + * 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 { + ConnectorField, + ConnectorMappingsAttributes, + ConnectorTypes, +} from '../../../common/api/connectors'; +import { + JiraGetFieldsResponse, + ResilientGetFieldsResponse, + ServiceNowGetFieldsResponse, +} from './utils.test'; +interface TestMappings { + [key: string]: ConnectorMappingsAttributes[]; +} +export const mappings: TestMappings = { + [ConnectorTypes.jira]: [ + { + source: 'title', + target: 'summary', + action_type: 'overwrite', + }, + { + source: 'description', + target: 'description', + action_type: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + action_type: 'append', + }, + ], + [`${ConnectorTypes.jira}-alt`]: [ + { + source: 'title', + target: 'title', + action_type: 'overwrite', + }, + { + source: 'description', + target: 'description', + action_type: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + action_type: 'append', + }, + ], + [ConnectorTypes.resilient]: [ + { + source: 'title', + target: 'name', + action_type: 'overwrite', + }, + { + source: 'description', + target: 'description', + action_type: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + action_type: 'append', + }, + ], + [ConnectorTypes.servicenow]: [ + { + source: 'title', + target: 'short_description', + action_type: 'overwrite', + }, + { + source: 'description', + target: 'description', + action_type: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + action_type: 'append', + }, + ], +}; + +const jiraFields: JiraGetFieldsResponse = { + summary: { + required: true, + allowedValues: [], + defaultValue: {}, + schema: { + type: 'string', + }, + name: 'Summary', + }, + issuetype: { + required: true, + allowedValues: [ + { + self: 'https://siem-kibana.atlassian.net/rest/api/2/issuetype/10023', + id: '10023', + description: 'A problem or error.', + iconUrl: + 'https://siem-kibana.atlassian.net/secure/viewavatar?size=medium&avatarId=10303&avatarType=issuetype', + name: 'Bug', + subtask: false, + avatarId: 10303, + }, + ], + defaultValue: {}, + schema: { + type: 'issuetype', + }, + name: 'Issue Type', + }, + attachment: { + required: false, + allowedValues: [], + defaultValue: {}, + schema: { + type: 'array', + items: 'attachment', + }, + name: 'Attachment', + }, + duedate: { + required: false, + allowedValues: [], + defaultValue: {}, + schema: { + type: 'date', + }, + name: 'Due date', + }, + description: { + required: false, + allowedValues: [], + defaultValue: {}, + schema: { + type: 'string', + }, + name: 'Description', + }, + project: { + required: true, + allowedValues: [ + { + self: 'https://siem-kibana.atlassian.net/rest/api/2/project/10015', + id: '10015', + key: 'RJ2', + name: 'RJ2', + projectTypeKey: 'business', + simplified: false, + avatarUrls: { + '48x48': + 'https://siem-kibana.atlassian.net/secure/projectavatar?pid=10015&avatarId=10412', + '24x24': + 'https://siem-kibana.atlassian.net/secure/projectavatar?size=small&s=small&pid=10015&avatarId=10412', + '16x16': + 'https://siem-kibana.atlassian.net/secure/projectavatar?size=xsmall&s=xsmall&pid=10015&avatarId=10412', + '32x32': + 'https://siem-kibana.atlassian.net/secure/projectavatar?size=medium&s=medium&pid=10015&avatarId=10412', + }, + }, + ], + defaultValue: {}, + schema: { + type: 'project', + }, + name: 'Project', + }, + assignee: { + required: false, + allowedValues: [], + defaultValue: {}, + schema: { + type: 'user', + }, + name: 'Assignee', + }, + labels: { + required: false, + allowedValues: [], + defaultValue: {}, + schema: { + type: 'array', + items: 'string', + }, + name: 'Labels', + }, +}; +const resilientFields: ResilientGetFieldsResponse = [ + { input_type: 'text', name: 'addr', read_only: false, text: 'Address' }, + { + input_type: 'boolean', + name: 'alberta_health_risk_assessment', + read_only: false, + text: 'Alberta Health Risk Assessment', + }, + { input_type: 'number', name: 'hard_liability', read_only: true, text: 'Assessed Liability' }, + { input_type: 'text', name: 'city', read_only: false, text: 'City' }, + { input_type: 'select', name: 'country', read_only: false, text: 'Country/Region' }, + { input_type: 'select_owner', name: 'creator_id', read_only: true, text: 'Created By' }, + { input_type: 'select', name: 'crimestatus_id', read_only: false, text: 'Criminal Activity' }, + { input_type: 'boolean', name: 'data_encrypted', read_only: false, text: 'Data Encrypted' }, + { input_type: 'select', name: 'data_format', read_only: false, text: 'Data Format' }, + { input_type: 'datetimepicker', name: 'end_date', read_only: true, text: 'Date Closed' }, + { input_type: 'datetimepicker', name: 'create_date', read_only: true, text: 'Date Created' }, + { + input_type: 'datetimepicker', + name: 'determined_date', + read_only: false, + text: 'Date Determined', + }, + { + input_type: 'datetimepicker', + name: 'discovered_date', + read_only: false, + required: 'always', + text: 'Date Discovered', + }, + { input_type: 'datetimepicker', name: 'start_date', read_only: false, text: 'Date Occurred' }, + { input_type: 'select', name: 'exposure_dept_id', read_only: false, text: 'Department' }, + { input_type: 'textarea', name: 'description', read_only: false, text: 'Description' }, + { input_type: 'boolean', name: 'employee_involved', read_only: false, text: 'Employee Involved' }, + { input_type: 'boolean', name: 'data_contained', read_only: false, text: 'Exposure Resolved' }, + { input_type: 'select', name: 'exposure_type_id', read_only: false, text: 'Exposure Type' }, + { + input_type: 'multiselect', + name: 'gdpr_breach_circumstances', + read_only: false, + text: 'GDPR Breach Circumstances', + }, + { input_type: 'select', name: 'gdpr_breach_type', read_only: false, text: 'GDPR Breach Type' }, + { + input_type: 'textarea', + name: 'gdpr_breach_type_comment', + read_only: false, + text: 'GDPR Breach Type Comment', + }, + { input_type: 'select', name: 'gdpr_consequences', read_only: false, text: 'GDPR Consequences' }, + { + input_type: 'textarea', + name: 'gdpr_consequences_comment', + read_only: false, + text: 'GDPR Consequences Comment', + }, + { + input_type: 'select', + name: 'gdpr_final_assessment', + read_only: false, + text: 'GDPR Final Assessment', + }, + { + input_type: 'textarea', + name: 'gdpr_final_assessment_comment', + read_only: false, + text: 'GDPR Final Assessment Comment', + }, + { + input_type: 'select', + name: 'gdpr_identification', + read_only: false, + text: 'GDPR Identification', + }, + { + input_type: 'textarea', + name: 'gdpr_identification_comment', + read_only: false, + text: 'GDPR Identification Comment', + }, + { + input_type: 'select', + name: 'gdpr_personal_data', + read_only: false, + text: 'GDPR Personal Data', + }, + { + input_type: 'textarea', + name: 'gdpr_personal_data_comment', + read_only: false, + text: 'GDPR Personal Data Comment', + }, + { + input_type: 'boolean', + name: 'gdpr_subsequent_notification', + read_only: false, + text: 'GDPR Subsequent Notification', + }, + { input_type: 'number', name: 'id', read_only: true, text: 'ID' }, + { input_type: 'boolean', name: 'impact_likely', read_only: false, text: 'Impact Likely' }, + { + input_type: 'boolean', + name: 'ny_impact_likely', + read_only: false, + text: 'Impact Likely for New York', + }, + { + input_type: 'boolean', + name: 'or_impact_likely', + read_only: false, + text: 'Impact Likely for Oregon', + }, + { + input_type: 'boolean', + name: 'wa_impact_likely', + read_only: false, + text: 'Impact Likely for Washington', + }, + { input_type: 'boolean', name: 'confirmed', read_only: false, text: 'Incident Disposition' }, + { input_type: 'multiselect', name: 'incident_type_ids', read_only: false, text: 'Incident Type' }, + { + input_type: 'text', + name: 'exposure_individual_name', + read_only: false, + text: 'Individual Name', + }, + { + input_type: 'select', + name: 'harmstatus_id', + read_only: false, + text: 'Is harm/risk/misuse foreseeable?', + }, + { input_type: 'text', name: 'jurisdiction_name', read_only: false, text: 'Jurisdiction' }, + { + input_type: 'datetimepicker', + name: 'inc_last_modified_date', + read_only: true, + text: 'Last Modified', + }, + { + input_type: 'multiselect', + name: 'gdpr_lawful_data_processing_categories', + read_only: false, + text: 'Lawful Data Processing Categories', + }, + { input_type: 'multiselect_members', name: 'members', read_only: false, text: 'Members' }, + { input_type: 'text', name: 'name', read_only: false, required: 'always', text: 'Name' }, + { input_type: 'boolean', name: 'negative_pr_likely', read_only: false, text: 'Negative PR' }, + { input_type: 'datetimepicker', name: 'due_date', read_only: true, text: 'Next Due Date' }, + { + input_type: 'multiselect', + name: 'nist_attack_vectors', + read_only: false, + text: 'NIST Attack Vectors', + }, + { input_type: 'select', name: 'org_handle', read_only: true, text: 'Organization' }, + { input_type: 'select_owner', name: 'owner_id', read_only: false, text: 'Owner' }, + { input_type: 'select', name: 'phase_id', read_only: true, text: 'Phase' }, + { + input_type: 'select', + name: 'pipeda_other_factors', + read_only: false, + text: 'PIPEDA Other Factors', + }, + { + input_type: 'textarea', + name: 'pipeda_other_factors_comment', + read_only: false, + text: 'PIPEDA Other Factors Comment', + }, + { + input_type: 'select', + name: 'pipeda_overall_assessment', + read_only: false, + text: 'PIPEDA Overall Assessment', + }, + { + input_type: 'textarea', + name: 'pipeda_overall_assessment_comment', + read_only: false, + text: 'PIPEDA Overall Assessment Comment', + }, + { + input_type: 'select', + name: 'pipeda_probability_of_misuse', + read_only: false, + text: 'PIPEDA Probability of Misuse', + }, + { + input_type: 'textarea', + name: 'pipeda_probability_of_misuse_comment', + read_only: false, + text: 'PIPEDA Probability of Misuse Comment', + }, + { + input_type: 'select', + name: 'pipeda_sensitivity_of_pi', + read_only: false, + text: 'PIPEDA Sensitivity of PI', + }, + { + input_type: 'textarea', + name: 'pipeda_sensitivity_of_pi_comment', + read_only: false, + text: 'PIPEDA Sensitivity of PI Comment', + }, + { input_type: 'text', name: 'reporter', read_only: false, text: 'Reporting Individual' }, + { + input_type: 'select', + name: 'resolution_id', + read_only: false, + required: 'close', + text: 'Resolution', + }, + { + input_type: 'textarea', + name: 'resolution_summary', + read_only: false, + required: 'close', + text: 'Resolution Summary', + }, + { input_type: 'select', name: 'gdpr_harm_risk', read_only: false, text: 'Risk of Harm' }, + { input_type: 'select', name: 'severity_code', read_only: false, text: 'Severity' }, + { input_type: 'boolean', name: 'inc_training', read_only: true, text: 'Simulation' }, + { input_type: 'multiselect', name: 'data_source_ids', read_only: false, text: 'Source of Data' }, + { input_type: 'select', name: 'state', read_only: false, text: 'State' }, + { input_type: 'select', name: 'plan_status', read_only: false, text: 'Status' }, + { input_type: 'select', name: 'exposure_vendor_id', read_only: false, text: 'Vendor' }, + { + input_type: 'boolean', + name: 'data_compromised', + read_only: false, + text: 'Was personal information or personal data involved?', + }, + { + input_type: 'select', + name: 'workspace', + read_only: false, + required: 'always', + text: 'Workspace', + }, + { input_type: 'text', name: 'zip', read_only: false, text: 'Zip' }, +]; +const serviceNowFields: ServiceNowGetFieldsResponse = [ + { + column_label: 'Approval', + mandatory: 'false', + max_length: '40', + element: 'approval', + }, + { + column_label: 'Close notes', + mandatory: 'false', + max_length: '4000', + element: 'close_notes', + }, + { + column_label: 'Contact type', + mandatory: 'false', + max_length: '40', + element: 'contact_type', + }, + { + column_label: 'Correlation display', + mandatory: 'false', + max_length: '100', + element: 'correlation_display', + }, + { + column_label: 'Correlation ID', + mandatory: 'false', + max_length: '100', + element: 'correlation_id', + }, + { + column_label: 'Description', + mandatory: 'false', + max_length: '4000', + element: 'description', + }, + { + column_label: 'Number', + mandatory: 'false', + max_length: '40', + element: 'number', + }, + { + column_label: 'Short description', + mandatory: 'false', + max_length: '160', + element: 'short_description', + }, + { + column_label: 'Created by', + mandatory: 'false', + max_length: '40', + element: 'sys_created_by', + }, + { + column_label: 'Updated by', + mandatory: 'false', + max_length: '40', + element: 'sys_updated_by', + }, + { + column_label: 'Upon approval', + mandatory: 'false', + max_length: '40', + element: 'upon_approval', + }, + { + column_label: 'Upon reject', + mandatory: 'false', + max_length: '40', + element: 'upon_reject', + }, +]; +interface FormatFieldsTestData { + expected: ConnectorField[]; + fields: JiraGetFieldsResponse | ResilientGetFieldsResponse | ServiceNowGetFieldsResponse; + type: ConnectorTypes; +} +export const formatFieldsTestData: FormatFieldsTestData[] = [ + { + expected: [ + { id: 'summary', name: 'Summary', required: true, type: 'text' }, + { id: 'description', name: 'Description', required: false, type: 'text' }, + ], + fields: jiraFields, + type: ConnectorTypes.jira, + }, + { + expected: [ + { id: 'addr', name: 'Address', required: false, type: 'text' }, + { id: 'city', name: 'City', required: false, type: 'text' }, + { id: 'description', name: 'Description', required: false, type: 'textarea' }, + { + id: 'gdpr_breach_type_comment', + name: 'GDPR Breach Type Comment', + required: false, + type: 'textarea', + }, + { + id: 'gdpr_consequences_comment', + name: 'GDPR Consequences Comment', + required: false, + type: 'textarea', + }, + { + id: 'gdpr_final_assessment_comment', + name: 'GDPR Final Assessment Comment', + required: false, + type: 'textarea', + }, + { + id: 'gdpr_identification_comment', + name: 'GDPR Identification Comment', + required: false, + type: 'textarea', + }, + { + id: 'gdpr_personal_data_comment', + name: 'GDPR Personal Data Comment', + required: false, + type: 'textarea', + }, + { id: 'exposure_individual_name', name: 'Individual Name', required: false, type: 'text' }, + { id: 'jurisdiction_name', name: 'Jurisdiction', required: false, type: 'text' }, + { id: 'name', name: 'Name', required: true, type: 'text' }, + { + id: 'pipeda_other_factors_comment', + name: 'PIPEDA Other Factors Comment', + required: false, + type: 'textarea', + }, + { + id: 'pipeda_overall_assessment_comment', + name: 'PIPEDA Overall Assessment Comment', + required: false, + type: 'textarea', + }, + { + id: 'pipeda_probability_of_misuse_comment', + name: 'PIPEDA Probability of Misuse Comment', + required: false, + type: 'textarea', + }, + { + id: 'pipeda_sensitivity_of_pi_comment', + name: 'PIPEDA Sensitivity of PI Comment', + required: false, + type: 'textarea', + }, + { id: 'reporter', name: 'Reporting Individual', required: false, type: 'text' }, + { id: 'resolution_summary', name: 'Resolution Summary', required: false, type: 'textarea' }, + { id: 'zip', name: 'Zip', required: false, type: 'text' }, + ], + fields: resilientFields, + type: ConnectorTypes.resilient, + }, + { + expected: [ + { id: 'approval', name: 'Approval', required: false, type: 'text' }, + { id: 'close_notes', name: 'Close notes', required: false, type: 'textarea' }, + { id: 'contact_type', name: 'Contact type', required: false, type: 'text' }, + { id: 'correlation_display', name: 'Correlation display', required: false, type: 'text' }, + { id: 'correlation_id', name: 'Correlation ID', required: false, type: 'text' }, + { id: 'description', name: 'Description', required: false, type: 'textarea' }, + { id: 'number', name: 'Number', required: false, type: 'text' }, + { id: 'short_description', name: 'Short description', required: false, type: 'text' }, + { id: 'sys_created_by', name: 'Created by', required: false, type: 'text' }, + { id: 'sys_updated_by', name: 'Updated by', required: false, type: 'text' }, + { id: 'upon_approval', name: 'Upon approval', required: false, type: 'text' }, + { id: 'upon_reject', name: 'Upon reject', required: false, type: 'text' }, + ], + fields: serviceNowFields, + type: ConnectorTypes.servicenow, + }, +]; +export const mockGetFieldsResponse = { + status: 'ok', + data: jiraFields, + actionId: '123', +}; + +export const actionsErrResponse = { + status: 'error', + serviceMessage: 'this is an actions error', +}; diff --git a/x-pack/plugins/case/server/client/configure/utils.test.ts b/x-pack/plugins/case/server/client/configure/utils.test.ts index 91c8259cb2c555..f4f0e077425218 100644 --- a/x-pack/plugins/case/server/client/configure/utils.test.ts +++ b/x-pack/plugins/case/server/client/configure/utils.test.ts @@ -4,535 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { +export { JiraGetFieldsResponse, ResilientGetFieldsResponse, ServiceNowGetFieldsResponse, } from '../../../../actions/server/types'; -import { formatFields } from './utils'; +import { createDefaultMapping, formatFields } from './utils'; import { ConnectorTypes } from '../../../common/api/connectors'; +import { mappings, formatFieldsTestData } from './mock'; -const jiraFields: JiraGetFieldsResponse = { - summary: { - required: true, - allowedValues: [], - defaultValue: {}, - schema: { - type: 'string', - }, - name: 'Summary', - }, - issuetype: { - required: true, - allowedValues: [ - { - self: 'https://siem-kibana.atlassian.net/rest/api/2/issuetype/10023', - id: '10023', - description: 'A problem or error.', - iconUrl: - 'https://siem-kibana.atlassian.net/secure/viewavatar?size=medium&avatarId=10303&avatarType=issuetype', - name: 'Bug', - subtask: false, - avatarId: 10303, - }, - ], - defaultValue: {}, - schema: { - type: 'issuetype', - }, - name: 'Issue Type', - }, - attachment: { - required: false, - allowedValues: [], - defaultValue: {}, - schema: { - type: 'array', - items: 'attachment', - }, - name: 'Attachment', - }, - duedate: { - required: false, - allowedValues: [], - defaultValue: {}, - schema: { - type: 'date', - }, - name: 'Due date', - }, - description: { - required: false, - allowedValues: [], - defaultValue: {}, - schema: { - type: 'string', - }, - name: 'Description', - }, - project: { - required: true, - allowedValues: [ - { - self: 'https://siem-kibana.atlassian.net/rest/api/2/project/10015', - id: '10015', - key: 'RJ2', - name: 'RJ2', - projectTypeKey: 'business', - simplified: false, - avatarUrls: { - '48x48': - 'https://siem-kibana.atlassian.net/secure/projectavatar?pid=10015&avatarId=10412', - '24x24': - 'https://siem-kibana.atlassian.net/secure/projectavatar?size=small&s=small&pid=10015&avatarId=10412', - '16x16': - 'https://siem-kibana.atlassian.net/secure/projectavatar?size=xsmall&s=xsmall&pid=10015&avatarId=10412', - '32x32': - 'https://siem-kibana.atlassian.net/secure/projectavatar?size=medium&s=medium&pid=10015&avatarId=10412', - }, - }, - ], - defaultValue: {}, - schema: { - type: 'project', - }, - name: 'Project', - }, - assignee: { - required: false, - allowedValues: [], - defaultValue: {}, - schema: { - type: 'user', - }, - name: 'Assignee', - }, - labels: { - required: false, - allowedValues: [], - defaultValue: {}, - schema: { - type: 'array', - items: 'string', - }, - name: 'Labels', - }, -}; -const resilientFields: ResilientGetFieldsResponse = [ - { input_type: 'text', name: 'addr', read_only: false, text: 'Address' }, - { - input_type: 'boolean', - name: 'alberta_health_risk_assessment', - read_only: false, - text: 'Alberta Health Risk Assessment', - }, - { input_type: 'number', name: 'hard_liability', read_only: true, text: 'Assessed Liability' }, - { input_type: 'text', name: 'city', read_only: false, text: 'City' }, - { input_type: 'select', name: 'country', read_only: false, text: 'Country/Region' }, - { input_type: 'select_owner', name: 'creator_id', read_only: true, text: 'Created By' }, - { input_type: 'select', name: 'crimestatus_id', read_only: false, text: 'Criminal Activity' }, - { input_type: 'boolean', name: 'data_encrypted', read_only: false, text: 'Data Encrypted' }, - { input_type: 'select', name: 'data_format', read_only: false, text: 'Data Format' }, - { input_type: 'datetimepicker', name: 'end_date', read_only: true, text: 'Date Closed' }, - { input_type: 'datetimepicker', name: 'create_date', read_only: true, text: 'Date Created' }, - { - input_type: 'datetimepicker', - name: 'determined_date', - read_only: false, - text: 'Date Determined', - }, - { - input_type: 'datetimepicker', - name: 'discovered_date', - read_only: false, - required: 'always', - text: 'Date Discovered', - }, - { input_type: 'datetimepicker', name: 'start_date', read_only: false, text: 'Date Occurred' }, - { input_type: 'select', name: 'exposure_dept_id', read_only: false, text: 'Department' }, - { input_type: 'textarea', name: 'description', read_only: false, text: 'Description' }, - { input_type: 'boolean', name: 'employee_involved', read_only: false, text: 'Employee Involved' }, - { input_type: 'boolean', name: 'data_contained', read_only: false, text: 'Exposure Resolved' }, - { input_type: 'select', name: 'exposure_type_id', read_only: false, text: 'Exposure Type' }, - { - input_type: 'multiselect', - name: 'gdpr_breach_circumstances', - read_only: false, - text: 'GDPR Breach Circumstances', - }, - { input_type: 'select', name: 'gdpr_breach_type', read_only: false, text: 'GDPR Breach Type' }, - { - input_type: 'textarea', - name: 'gdpr_breach_type_comment', - read_only: false, - text: 'GDPR Breach Type Comment', - }, - { input_type: 'select', name: 'gdpr_consequences', read_only: false, text: 'GDPR Consequences' }, - { - input_type: 'textarea', - name: 'gdpr_consequences_comment', - read_only: false, - text: 'GDPR Consequences Comment', - }, - { - input_type: 'select', - name: 'gdpr_final_assessment', - read_only: false, - text: 'GDPR Final Assessment', - }, - { - input_type: 'textarea', - name: 'gdpr_final_assessment_comment', - read_only: false, - text: 'GDPR Final Assessment Comment', - }, - { - input_type: 'select', - name: 'gdpr_identification', - read_only: false, - text: 'GDPR Identification', - }, - { - input_type: 'textarea', - name: 'gdpr_identification_comment', - read_only: false, - text: 'GDPR Identification Comment', - }, - { - input_type: 'select', - name: 'gdpr_personal_data', - read_only: false, - text: 'GDPR Personal Data', - }, - { - input_type: 'textarea', - name: 'gdpr_personal_data_comment', - read_only: false, - text: 'GDPR Personal Data Comment', - }, - { - input_type: 'boolean', - name: 'gdpr_subsequent_notification', - read_only: false, - text: 'GDPR Subsequent Notification', - }, - { input_type: 'number', name: 'id', read_only: true, text: 'ID' }, - { input_type: 'boolean', name: 'impact_likely', read_only: false, text: 'Impact Likely' }, - { - input_type: 'boolean', - name: 'ny_impact_likely', - read_only: false, - text: 'Impact Likely for New York', - }, - { - input_type: 'boolean', - name: 'or_impact_likely', - read_only: false, - text: 'Impact Likely for Oregon', - }, - { - input_type: 'boolean', - name: 'wa_impact_likely', - read_only: false, - text: 'Impact Likely for Washington', - }, - { input_type: 'boolean', name: 'confirmed', read_only: false, text: 'Incident Disposition' }, - { input_type: 'multiselect', name: 'incident_type_ids', read_only: false, text: 'Incident Type' }, - { - input_type: 'text', - name: 'exposure_individual_name', - read_only: false, - text: 'Individual Name', - }, - { - input_type: 'select', - name: 'harmstatus_id', - read_only: false, - text: 'Is harm/risk/misuse foreseeable?', - }, - { input_type: 'text', name: 'jurisdiction_name', read_only: false, text: 'Jurisdiction' }, - { - input_type: 'datetimepicker', - name: 'inc_last_modified_date', - read_only: true, - text: 'Last Modified', - }, - { - input_type: 'multiselect', - name: 'gdpr_lawful_data_processing_categories', - read_only: false, - text: 'Lawful Data Processing Categories', - }, - { input_type: 'multiselect_members', name: 'members', read_only: false, text: 'Members' }, - { input_type: 'text', name: 'name', read_only: false, required: 'always', text: 'Name' }, - { input_type: 'boolean', name: 'negative_pr_likely', read_only: false, text: 'Negative PR' }, - { input_type: 'datetimepicker', name: 'due_date', read_only: true, text: 'Next Due Date' }, - { - input_type: 'multiselect', - name: 'nist_attack_vectors', - read_only: false, - text: 'NIST Attack Vectors', - }, - { input_type: 'select', name: 'org_handle', read_only: true, text: 'Organization' }, - { input_type: 'select_owner', name: 'owner_id', read_only: false, text: 'Owner' }, - { input_type: 'select', name: 'phase_id', read_only: true, text: 'Phase' }, - { - input_type: 'select', - name: 'pipeda_other_factors', - read_only: false, - text: 'PIPEDA Other Factors', - }, - { - input_type: 'textarea', - name: 'pipeda_other_factors_comment', - read_only: false, - text: 'PIPEDA Other Factors Comment', - }, - { - input_type: 'select', - name: 'pipeda_overall_assessment', - read_only: false, - text: 'PIPEDA Overall Assessment', - }, - { - input_type: 'textarea', - name: 'pipeda_overall_assessment_comment', - read_only: false, - text: 'PIPEDA Overall Assessment Comment', - }, - { - input_type: 'select', - name: 'pipeda_probability_of_misuse', - read_only: false, - text: 'PIPEDA Probability of Misuse', - }, - { - input_type: 'textarea', - name: 'pipeda_probability_of_misuse_comment', - read_only: false, - text: 'PIPEDA Probability of Misuse Comment', - }, - { - input_type: 'select', - name: 'pipeda_sensitivity_of_pi', - read_only: false, - text: 'PIPEDA Sensitivity of PI', - }, - { - input_type: 'textarea', - name: 'pipeda_sensitivity_of_pi_comment', - read_only: false, - text: 'PIPEDA Sensitivity of PI Comment', - }, - { input_type: 'text', name: 'reporter', read_only: false, text: 'Reporting Individual' }, - { - input_type: 'select', - name: 'resolution_id', - read_only: false, - required: 'close', - text: 'Resolution', - }, - { - input_type: 'textarea', - name: 'resolution_summary', - read_only: false, - required: 'close', - text: 'Resolution Summary', - }, - { input_type: 'select', name: 'gdpr_harm_risk', read_only: false, text: 'Risk of Harm' }, - { input_type: 'select', name: 'severity_code', read_only: false, text: 'Severity' }, - { input_type: 'boolean', name: 'inc_training', read_only: true, text: 'Simulation' }, - { input_type: 'multiselect', name: 'data_source_ids', read_only: false, text: 'Source of Data' }, - { input_type: 'select', name: 'state', read_only: false, text: 'State' }, - { input_type: 'select', name: 'plan_status', read_only: false, text: 'Status' }, - { input_type: 'select', name: 'exposure_vendor_id', read_only: false, text: 'Vendor' }, - { - input_type: 'boolean', - name: 'data_compromised', - read_only: false, - text: 'Was personal information or personal data involved?', - }, - { - input_type: 'select', - name: 'workspace', - read_only: false, - required: 'always', - text: 'Workspace', - }, - { input_type: 'text', name: 'zip', read_only: false, text: 'Zip' }, -]; -const serviceNowFields: ServiceNowGetFieldsResponse = [ - { - column_label: 'Approval', - mandatory: 'false', - max_length: '40', - element: 'approval', - }, - { - column_label: 'Close notes', - mandatory: 'false', - max_length: '4000', - element: 'close_notes', - }, - { - column_label: 'Contact type', - mandatory: 'false', - max_length: '40', - element: 'contact_type', - }, - { - column_label: 'Correlation display', - mandatory: 'false', - max_length: '100', - element: 'correlation_display', - }, - { - column_label: 'Correlation ID', - mandatory: 'false', - max_length: '100', - element: 'correlation_id', - }, - { - column_label: 'Description', - mandatory: 'false', - max_length: '4000', - element: 'description', - }, - { - column_label: 'Number', - mandatory: 'false', - max_length: '40', - element: 'number', - }, - { - column_label: 'Short description', - mandatory: 'false', - max_length: '160', - element: 'short_description', - }, - { - column_label: 'Created by', - mandatory: 'false', - max_length: '40', - element: 'sys_created_by', - }, - { - column_label: 'Updated by', - mandatory: 'false', - max_length: '40', - element: 'sys_updated_by', - }, - { - column_label: 'Upon approval', - mandatory: 'false', - max_length: '40', - element: 'upon_approval', - }, - { - column_label: 'Upon reject', - mandatory: 'false', - max_length: '40', - element: 'upon_reject', - }, -]; - -const formatFieldsTestData = [ - { - expected: [ - { id: 'summary', name: 'Summary', required: true, type: 'text' }, - { id: 'description', name: 'Description', required: false, type: 'text' }, - ], - fields: jiraFields, - type: ConnectorTypes.jira, - }, - { - expected: [ - { id: 'addr', name: 'Address', required: false, type: 'text' }, - { id: 'city', name: 'City', required: false, type: 'text' }, - { id: 'description', name: 'Description', required: false, type: 'textarea' }, - { - id: 'gdpr_breach_type_comment', - name: 'GDPR Breach Type Comment', - required: false, - type: 'textarea', - }, - { - id: 'gdpr_consequences_comment', - name: 'GDPR Consequences Comment', - required: false, - type: 'textarea', - }, - { - id: 'gdpr_final_assessment_comment', - name: 'GDPR Final Assessment Comment', - required: false, - type: 'textarea', - }, - { - id: 'gdpr_identification_comment', - name: 'GDPR Identification Comment', - required: false, - type: 'textarea', - }, - { - id: 'gdpr_personal_data_comment', - name: 'GDPR Personal Data Comment', - required: false, - type: 'textarea', - }, - { id: 'exposure_individual_name', name: 'Individual Name', required: false, type: 'text' }, - { id: 'jurisdiction_name', name: 'Jurisdiction', required: false, type: 'text' }, - { id: 'name', name: 'Name', required: true, type: 'text' }, - { - id: 'pipeda_other_factors_comment', - name: 'PIPEDA Other Factors Comment', - required: false, - type: 'textarea', - }, - { - id: 'pipeda_overall_assessment_comment', - name: 'PIPEDA Overall Assessment Comment', - required: false, - type: 'textarea', - }, - { - id: 'pipeda_probability_of_misuse_comment', - name: 'PIPEDA Probability of Misuse Comment', - required: false, - type: 'textarea', - }, - { - id: 'pipeda_sensitivity_of_pi_comment', - name: 'PIPEDA Sensitivity of PI Comment', - required: false, - type: 'textarea', - }, - { id: 'reporter', name: 'Reporting Individual', required: false, type: 'text' }, - { id: 'resolution_summary', name: 'Resolution Summary', required: false, type: 'textarea' }, - { id: 'zip', name: 'Zip', required: false, type: 'text' }, - ], - fields: resilientFields, - type: ConnectorTypes.resilient, - }, - { - expected: [ - { id: 'approval', name: 'Approval', required: false, type: 'text' }, - { id: 'close_notes', name: 'Close notes', required: false, type: 'textarea' }, - { id: 'contact_type', name: 'Contact type', required: false, type: 'text' }, - { id: 'correlation_display', name: 'Correlation display', required: false, type: 'text' }, - { id: 'correlation_id', name: 'Correlation ID', required: false, type: 'text' }, - { id: 'description', name: 'Description', required: false, type: 'textarea' }, - { id: 'number', name: 'Number', required: false, type: 'text' }, - { id: 'short_description', name: 'Short description', required: false, type: 'text' }, - { id: 'sys_created_by', name: 'Created by', required: false, type: 'text' }, - { id: 'sys_updated_by', name: 'Updated by', required: false, type: 'text' }, - { id: 'upon_approval', name: 'Upon approval', required: false, type: 'text' }, - { id: 'upon_reject', name: 'Upon reject', required: false, type: 'text' }, - ], - fields: serviceNowFields, - type: ConnectorTypes.servicenow, - }, -]; describe('client/configure/utils', () => { describe('formatFields', () => { formatFieldsTestData.forEach(({ expected, fields, type }) => { @@ -542,4 +22,23 @@ describe('client/configure/utils', () => { }); }); }); + describe('createDefaultMapping', () => { + formatFieldsTestData.forEach(({ expected, fields, type }) => { + it(`normalizes ${type} fields to common type ConnectorField`, () => { + const result = createDefaultMapping(expected, type); + expect(result).toEqual(mappings[type]); + }); + }); + it(`if the preferredField is not required and another field is, use the other field`, () => { + const result = createDefaultMapping( + [ + { id: 'summary', name: 'Summary', required: false, type: 'text' }, + { id: 'title', name: 'Title', required: true, type: 'text' }, + { id: 'description', name: 'Description', required: false, type: 'text' }, + ], + ConnectorTypes.jira + ); + expect(result).toEqual(mappings[`${ConnectorTypes.jira}-alt`]); + }); + }); }); diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts index 45ccb4f2c539fa..0d78bceeaf2fa9 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; +import { SavedObject } from 'kibana/server'; import { CaseStatuses, CommentAttributes, @@ -14,8 +14,8 @@ import { ESCaseAttributes, ESCasesConfigureAttributes, } from '../../../../common/api'; -import { mappings } from '../cases/configure/mock'; import { CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT } from '../../../saved_object_types'; +import { mappings } from '../../../client/configure/mock'; export const mockCases: Array> = [ { @@ -381,31 +381,13 @@ export const mockCaseConfigure: Array> = }, ]; -export const mockCaseConfigureFind: Array> = [ - { - page: 1, - per_page: 5, - total: mockCaseConfigure.length, - saved_objects: [{ ...mockCaseConfigure[0], score: 0 }], - }, -]; - export const mockCaseMappings: Array> = [ { type: CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, id: 'mock-mappings-1', attributes: { - mappings, + mappings: mappings[ConnectorTypes.jira], }, references: [], }, ]; - -export const mockCaseMappingsFind: Array> = [ - { - page: 1, - per_page: 5, - total: mockCaseConfigure.length, - saved_objects: [{ ...mockCaseMappings[0], score: 0 }], - }, -]; diff --git a/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts b/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts index b6da21927e342e..efc3b6044a8045 100644 --- a/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts +++ b/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts @@ -3,9 +3,15 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { CasePostRequest, CasesConfigureRequest, ConnectorTypes } from '../../../../common/api'; +import { + CasePostRequest, + CasesConfigureRequest, + ConnectorTypes, + PostPushRequest, +} from '../../../../common/api'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { FindActionResult } from '../../../../../actions/server/types'; +import { params } from '../cases/configure/mock'; export const newCase: CasePostRequest = { title: 'My new case', @@ -76,3 +82,19 @@ export const newConfiguration: CasesConfigureRequest = { }, closure_type: 'close-by-pushing', }; + +export const newPostPushRequest: PostPushRequest = { + params: params[ConnectorTypes.jira], + connector_type: ConnectorTypes.jira, +}; + +export const executePushResponse = { + status: 'ok', + data: { + title: 'RJ2-200', + id: '10663', + pushedDate: '2020-12-17T00:32:40.738Z', + url: 'https://siem-kibana.atlassian.net/browse/RJ2-200', + comments: [], + }, +}; diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts index d75f42f6e486bc..87e165f8e00147 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts @@ -17,7 +17,8 @@ import { import { initGetCaseConfigure } from './get_configure'; import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; -import { mappings } from './mock'; +import { mappings } from '../../../../client/configure/mock'; +import { ConnectorTypes } from '../../../../../common/api/connectors'; describe('GET configuration', () => { let routeHandler: RequestHandler; @@ -42,7 +43,7 @@ describe('GET configuration', () => { expect(res.status).toEqual(200); expect(res.payload).toEqual({ ...mockCaseConfigure[0].attributes, - mappings, + mappings: mappings[ConnectorTypes.jira], version: mockCaseConfigure[0].version, }); }); @@ -76,7 +77,7 @@ describe('GET configuration', () => { email: 'testemail@elastic.co', username: 'elastic', }, - mappings, + mappings: mappings[ConnectorTypes.jira], updated_at: '2020-04-09T09:43:51.778Z', updated_by: { full_name: 'elastic', diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_fields.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_fields.ts deleted file mode 100644 index c9b8e671b7df82..00000000000000 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_fields.ts +++ /dev/null @@ -1,70 +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 Boom from '@hapi/boom'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; -import { RouteDeps } from '../../types'; -import { escapeHatch, wrapError } from '../../utils'; - -import { CASE_CONFIGURE_CONNECTOR_DETAILS_URL } from '../../../../../common/constants'; -import { - ConnectorRequestParamsRt, - GetFieldsRequestQueryRt, - throwErrors, -} from '../../../../../common/api'; - -export function initCaseConfigureGetFields({ router }: RouteDeps) { - router.get( - { - path: CASE_CONFIGURE_CONNECTOR_DETAILS_URL, - validate: { - params: escapeHatch, - query: escapeHatch, - }, - }, - async (context, request, response) => { - try { - if (!context.case) { - throw Boom.badRequest('RouteHandlerContext is not registered for cases'); - } - const query = pipe( - GetFieldsRequestQueryRt.decode(request.query), - fold(throwErrors(Boom.badRequest), identity) - ); - const params = pipe( - ConnectorRequestParamsRt.decode(request.params), - fold(throwErrors(Boom.badRequest), identity) - ); - - const caseClient = context.case.getCaseClient(); - - const connectorType = query.connector_type; - if (connectorType == null) { - throw Boom.illegal('no connectorType value provided'); - } - - const actionsClient = await context.actions?.getActionsClient(); - if (actionsClient == null) { - throw Boom.notFound('Action client have not been found'); - } - - const res = await caseClient.getFields({ - actionsClient, - connectorId: params.connector_id, - connectorType, - }); - - return response.ok({ - body: res.fields, - }); - } catch (error) { - return response.customError(wrapError(error)); - } - } - ); -} diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/mock.ts b/x-pack/plugins/case/server/routes/api/cases/configure/mock.ts index ed8b2088646115..771b09cec2a359 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/mock.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/mock.ts @@ -7,6 +7,7 @@ import { ServiceConnectorCaseParams, ServiceConnectorCommentParams, ConnectorMappingsAttributes, + ConnectorTypes, } from '../../../../../common/api/connectors'; export const updateUser = { updatedAt: '2020-03-13T08:34:53.450Z', @@ -24,16 +25,36 @@ export const comment: ServiceConnectorCommentParams = { ...entity, }; export const defaultPipes = ['informationCreated']; -export const params = { +const basicParams = { comments: [comment], description: 'a description', - impact: '3', - savedObjectId: '1231231231232', - severity: '1', title: 'a title', - urgency: '2', - ...entity, -} as ServiceConnectorCaseParams; + savedObjectId: '1231231231232', + externalId: null, +}; +export const params = { + [ConnectorTypes.jira]: { + ...basicParams, + issueType: '10003', + priority: 'Highest', + parent: '5002', + ...entity, + } as ServiceConnectorCaseParams, + [ConnectorTypes.resilient]: { + ...basicParams, + incidentTypes: ['10003'], + severityCode: '1', + ...entity, + } as ServiceConnectorCaseParams, + [ConnectorTypes.servicenow]: { + ...basicParams, + impact: '3', + severity: '1', + urgency: '2', + ...entity, + } as ServiceConnectorCaseParams, + [ConnectorTypes.none]: {}, +}; export const mappings: ConnectorMappingsAttributes[] = [ { source: 'title', diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.test.ts new file mode 100644 index 00000000000000..ff0939fdcce1fa --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.test.ts @@ -0,0 +1,104 @@ +/* + * 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 { kibanaResponseFactory, RequestHandler, RequestHandlerContext } from 'src/core/server'; +import { httpServerMock } from 'src/core/server/mocks'; + +import { + createMockSavedObjectsRepository, + createRoute, + createRouteContext, + mockCaseMappings, +} from '../../__fixtures__'; + +import { initPostPushToService } from './post_push_to_service'; +import { executePushResponse, newPostPushRequest } from '../../__mocks__/request_responses'; +import { CASE_CONFIGURE_PUSH_URL } from '../../../../../common/constants'; + +describe('Post push to service', () => { + let routeHandler: RequestHandler; + const req = httpServerMock.createKibanaRequest({ + path: `${CASE_CONFIGURE_PUSH_URL}`, + method: 'post', + params: { + connector_id: '666', + }, + body: newPostPushRequest, + }); + let context: RequestHandlerContext; + beforeAll(async () => { + routeHandler = await createRoute(initPostPushToService, 'post'); + const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; + spyOnDate.mockImplementation(() => ({ + toISOString: jest.fn().mockReturnValue('2020-04-09T09:43:51.778Z'), + })); + context = await createRouteContext( + createMockSavedObjectsRepository({ + caseMappingsSavedObject: mockCaseMappings, + }) + ); + }); + + it('Happy path - posts success', async () => { + const betterContext = ({ + ...context, + actions: { + ...context.actions, + getActionsClient: () => { + const actions = context!.actions!.getActionsClient(); + return { + ...actions, + execute: jest.fn().mockImplementation(({ actionId }) => { + return { + status: 'ok', + data: { + title: 'RJ2-200', + id: '10663', + pushedDate: '2020-12-17T00:32:40.738Z', + url: 'https://siem-kibana.atlassian.net/browse/RJ2-200', + comments: [], + }, + actionId, + }; + }), + }; + }, + }, + } as unknown) as RequestHandlerContext; + + const res = await routeHandler(betterContext, req, kibanaResponseFactory); + + expect(res.status).toEqual(200); + expect(res.payload).toEqual({ + ...executePushResponse, + actionId: '666', + }); + }); + it('Unhappy path - context case missing', async () => { + const betterContext = ({ + ...context, + case: null, + } as unknown) as RequestHandlerContext; + + const res = await routeHandler(betterContext, req, kibanaResponseFactory); + expect(res.status).toEqual(400); + expect(res.payload.isBoom).toBeTruthy(); + expect(res.payload.output.payload.message).toEqual( + 'RouteHandlerContext is not registered for cases' + ); + }); + it('Unhappy path - context actions missing', async () => { + const betterContext = ({ + ...context, + actions: null, + } as unknown) as RequestHandlerContext; + + const res = await routeHandler(betterContext, req, kibanaResponseFactory); + expect(res.status).toEqual(404); + expect(res.payload.isBoom).toBeTruthy(); + expect(res.payload.output.payload.message).toEqual('Action client have not been found'); + }); +}); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.ts index 9c4c06c5f4e186..fb7a91d0463136 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.ts @@ -19,7 +19,7 @@ import { } from '../../../../../common/api'; import { mapIncident } from './utils'; -export function initPostPushToService({ router, connectorMappingsService }: RouteDeps) { +export function initPostPushToService({ router }: RouteDeps) { router.post( { path: CASE_CONFIGURE_PUSH_URL, diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/utils.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/utils.test.ts index d2ecdf61c882d7..d1f8391ad082a0 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/utils.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/utils.test.ts @@ -5,25 +5,31 @@ */ import { + mapIncident, prepareFieldsForTransformation, - transformFields, + serviceFormatter, transformComments, transformers, + transformFields, } from './utils'; -import { comment as commentObj, defaultPipes, mappings, params, updateUser } from './mock'; +import { comment as commentObj, mappings, defaultPipes, params, updateUser } from './mock'; import { - ServiceConnectorCaseParams, + ConnectorTypes, ExternalServiceParams, Incident, + ServiceConnectorCaseParams, } from '../../../../../common/api/connectors'; +import { actionsClientMock } from '../../../../../../actions/server/actions_client.mock'; +import { mappings as mappingsMock } from '../../../../client/configure/mock'; const formatComment = { commentId: commentObj.commentId, comment: commentObj.comment }; +const serviceNowParams = params[ConnectorTypes.servicenow] as ServiceConnectorCaseParams; describe('api/cases/configure/utils', () => { describe('prepareFieldsForTransformation', () => { test('prepare fields with defaults', () => { const res = prepareFieldsForTransformation({ defaultPipes, - params, + params: serviceNowParams, mappings, }); expect(res).toEqual([ @@ -46,7 +52,7 @@ describe('api/cases/configure/utils', () => { const res = prepareFieldsForTransformation({ defaultPipes: ['myTestPipe'], mappings, - params, + params: serviceNowParams, }); expect(res).toEqual([ { @@ -69,11 +75,11 @@ describe('api/cases/configure/utils', () => { const fields = prepareFieldsForTransformation({ defaultPipes, mappings, - params, + params: serviceNowParams, }); const res = transformFields({ - params, + params: serviceNowParams, fields, }); @@ -85,14 +91,14 @@ describe('api/cases/configure/utils', () => { test('transform fields for update correctly', () => { const fields = prepareFieldsForTransformation({ - params, + params: serviceNowParams, mappings, defaultPipes: ['informationUpdated'], }); const res = transformFields({ params: { - ...params, + ...serviceNowParams, updatedAt: '2020-03-15T08:34:53.450Z', updatedBy: { username: 'anotherUser', @@ -114,13 +120,13 @@ describe('api/cases/configure/utils', () => { test('add newline character to description', () => { const fields = prepareFieldsForTransformation({ - params, + params: serviceNowParams, mappings, defaultPipes: ['informationUpdated'], }); const res = transformFields({ - params, + params: serviceNowParams, fields, currentIncident: { short_description: 'first title', @@ -134,12 +140,12 @@ describe('api/cases/configure/utils', () => { const fields = prepareFieldsForTransformation({ defaultPipes, mappings, - params, + params: serviceNowParams, }); const res = transformFields({ params: { - ...params, + ...serviceNowParams, createdBy: { fullName: '', username: 'elastic' }, }, fields, @@ -155,12 +161,12 @@ describe('api/cases/configure/utils', () => { const fields = prepareFieldsForTransformation({ defaultPipes: ['informationUpdated'], mappings, - params, + params: serviceNowParams, }); const res = transformFields({ params: { - ...params, + ...serviceNowParams, updatedAt: '2020-03-15T08:34:53.450Z', updatedBy: { username: 'anotherUser', fullName: '' }, }, @@ -382,4 +388,142 @@ describe('api/cases/configure/utils', () => { }); }); }); + describe('mapIncident', () => { + let actionsMock = actionsClientMock.create(); + it('maps an external incident', async () => { + const res = await mapIncident( + actionsMock, + '123', + ConnectorTypes.servicenow, + mappingsMock[ConnectorTypes.servicenow], + serviceNowParams + ); + expect(res).toEqual({ + incident: { + description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + externalId: null, + impact: '3', + severity: '1', + short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + urgency: '2', + }, + comments: [ + { + comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + }, + ], + }); + }); + it('throws error if invalid service', async () => { + await mapIncident( + actionsMock, + '123', + 'invalid', + mappingsMock[ConnectorTypes.servicenow], + serviceNowParams + ).catch((e) => { + expect(e).not.toBeNull(); + expect(e).toEqual(new Error(`Invalid service`)); + }); + }); + it('updates an existing incident', async () => { + const existingIncidentData = { + description: 'fun description', + impact: '3', + severity: '3', + short_description: 'fun title', + urgency: '3', + }; + const execute = jest.fn().mockReturnValue(existingIncidentData); + actionsMock = { ...actionsMock, execute }; + const res = await mapIncident( + actionsMock, + '123', + ConnectorTypes.servicenow, + mappingsMock[ConnectorTypes.servicenow], + { ...serviceNowParams, externalId: '123' } + ); + expect(res).toEqual({ + incident: { + description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + externalId: '123', + impact: '3', + severity: '1', + short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + urgency: '2', + }, + comments: [ + { + comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + }, + ], + }); + }); + it('throws error when existing incident throws', async () => { + const execute = jest.fn().mockImplementation(() => { + throw new Error('exception'); + }); + actionsMock = { ...actionsMock, execute }; + await mapIncident( + actionsMock, + '123', + ConnectorTypes.servicenow, + mappingsMock[ConnectorTypes.servicenow], + { ...serviceNowParams, externalId: '123' } + ).catch((e) => { + expect(e).not.toBeNull(); + expect(e).toEqual( + new Error( + `Retrieving Incident by id 123 from ServiceNow failed with exception: Error: exception` + ) + ); + }); + }); + }); + + const connectors = [ + { + name: ConnectorTypes.jira, + result: { + incident: { + issueType: '10003', + parent: '5002', + priority: 'Highest', + }, + thirdPartyName: 'Jira', + }, + }, + { + name: ConnectorTypes.resilient, + result: { + incident: { + incidentTypes: ['10003'], + severityCode: '1', + }, + thirdPartyName: 'Resilient', + }, + }, + { + name: ConnectorTypes.servicenow, + result: { + incident: { + impact: '3', + severity: '1', + urgency: '2', + }, + thirdPartyName: 'ServiceNow', + }, + }, + ]; + describe('serviceFormatter', () => { + connectors.forEach((c) => + it(`formats ${c.name}`, () => { + const caseParams = params[c.name] as ServiceConnectorCaseParams; + const res = serviceFormatter(c.name, caseParams); + expect(res).toEqual(c.result); + }) + ); + }); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/utils.ts b/x-pack/plugins/case/server/routes/api/cases/configure/utils.ts index b8a37661fe9f77..89109af4cecb9d 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/utils.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/utils.ts @@ -25,9 +25,7 @@ import { TransformerArgs, TransformFieldsArgs, } from '../../../../../common/api'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ActionsClient } from '../../../../../../actions/server/actions_client'; - +import { ActionsClient } from '../../../../../../actions/server'; export const mapIncident = async ( actionsClient: ActionsClient, connectorId: string, @@ -59,13 +57,11 @@ export const mapIncident = async ( ); } } - const fields = prepareFieldsForTransformation({ defaultPipes, mappings, params, }); - const transformedFields = transformFields< ServiceConnectorCaseParams, ExternalServiceParams, diff --git a/x-pack/plugins/case/server/routes/api/index.ts b/x-pack/plugins/case/server/routes/api/index.ts index 587e43b218f446..15817b425021e9 100644 --- a/x-pack/plugins/case/server/routes/api/index.ts +++ b/x-pack/plugins/case/server/routes/api/index.ts @@ -25,7 +25,6 @@ import { initPostCommentApi } from './cases/comments/post_comment'; import { initCaseConfigureGetActionConnector } from './cases/configure/get_connectors'; import { initGetCaseConfigure } from './cases/configure/get_configure'; -import { initCaseConfigureGetFields } from './cases/configure/get_fields'; import { initPatchCaseConfigure } from './cases/configure/patch_configure'; import { initPostCaseConfigure } from './cases/configure/post_configure'; import { initPostPushToService } from './cases/configure/post_push_to_service'; @@ -54,7 +53,6 @@ export function initCaseApi(deps: RouteDeps) { initGetCaseConfigure(deps); initPatchCaseConfigure(deps); initPostCaseConfigure(deps); - initCaseConfigureGetFields(deps); initPostPushToService(deps); // Reporters initGetReportersApi(deps); diff --git a/x-pack/plugins/security_solution/public/cases/containers/api.ts b/x-pack/plugins/security_solution/public/cases/containers/api.ts index ef1e35b8ceb4b2..07f7391ca94d90 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/api.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/api.ts @@ -16,7 +16,6 @@ import { CaseUserActionsResponse, CommentRequest, CommentType, - ConnectorField, ServiceConnectorCaseParams, ServiceConnectorCaseResponse, User, @@ -24,7 +23,6 @@ import { import { ACTION_TYPES_URL, - CASE_CONFIGURE_CONNECTORS_URL, CASE_REPORTERS_URL, CASE_STATUS_URL, CASE_TAGS_URL, @@ -273,20 +271,3 @@ export const getActionLicense = async (signal: AbortSignal): Promise => { - const response = await KibanaServices.get().http.fetch( - `${CASE_CONFIGURE_CONNECTORS_URL}/${connectorId}`, - { - query: { - connector_type: connectorType, - }, - method: 'GET', - signal, - } - ); - return response; -}; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_fields.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_fields.tsx deleted file mode 100644 index 6b594fa60e0c73..00000000000000 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_fields.tsx +++ /dev/null @@ -1,82 +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 { useCallback, useEffect, useState } from 'react'; - -import { errorToToaster, useStateToaster } from '../../common/components/toasters'; -import { getFields } from './api'; -import * as i18n from './translations'; -import { ConnectorField } from '../../../../case/common/api'; - -interface FieldsState { - fields: ConnectorField[]; - isLoading: boolean; - isError: boolean; -} - -const initialData: FieldsState = { - fields: [], - isLoading: false, - isError: false, -}; - -export interface UseGetFields extends FieldsState { - fetchFields: () => void; -} - -export const useGetFields = (connectorId: string, connectorType: string): UseGetFields => { - const [fieldsState, setFieldsState] = useState(initialData); - const [, dispatchToaster] = useStateToaster(); - - const fetchFields = useCallback(() => { - let didCancel = false; - const abortCtrl = new AbortController(); - const fetchData = async () => { - setFieldsState({ - ...fieldsState, - isLoading: true, - }); - try { - const response = await getFields(connectorId, connectorType, abortCtrl.signal); - if (!didCancel) { - setFieldsState({ - fields: response, - isLoading: false, - isError: false, - }); - } - } catch (error) { - if (!didCancel) { - errorToToaster({ - title: i18n.ERROR_TITLE, - error: error.body && error.body.message ? new Error(error.body.message) : error, - dispatchToaster, - }); - setFieldsState({ - fields: [], - isLoading: false, - isError: true, - }); - } - } - }; - fetchData(); - return () => { - didCancel = true; - abortCtrl.abort(); - }; - }, [connectorId, connectorType, dispatchToaster, fieldsState]); - - useEffect(() => { - fetchFields(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return { - ...fieldsState, - fetchFields, - }; -};