From a9f2c9167337a8c2e07ae4f07129976a61d297c5 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 11 Feb 2021 13:08:39 +0200 Subject: [PATCH] [Security Solution][Case] ServiceNow ITSM: Add category & subcategory fields (#90547) --- x-pack/plugins/actions/README.md | 2 + .../servicenow/api.test.ts | 20 +++ .../builtin_action_types/servicenow/mocks.ts | 2 + .../builtin_action_types/servicenow/schema.ts | 6 +- .../case/common/api/connectors/jira.ts | 1 + .../case/common/api/connectors/resilient.ts | 1 + .../common/api/connectors/servicenow_itsm.ts | 3 + .../common/api/connectors/servicenow_sir.ts | 1 + .../case/server/connectors/case/index.test.ts | 24 ++- .../case/server/connectors/case/schema.ts | 2 + .../connectors/servicenow/itsm_formatter.ts | 4 +- .../servicenow/itsm_formmater.test.ts | 12 +- .../security_solution/cypress/objects/case.ts | 12 ++ .../cypress/tasks/create_new_case.ts | 2 +- .../cases/components/connectors/mock.ts | 12 ++ .../connectors/servicenow/helpers.ts | 12 ++ .../servicenow_itsm_case_fields.test.tsx | 67 +++++++- .../servicenow_itsm_case_fields.tsx | 122 ++++++++++++--- .../servicenow_sir_case_fields.test.tsx | 25 ++- .../servicenow/servicenow_sir_case_fields.tsx | 20 +-- .../components/connectors/servicenow/types.ts | 3 - .../components/create/connector.test.tsx | 8 +- .../components/create/form_context.test.tsx | 144 +++++++++++++++++- .../public/cases/components/create/mock.ts | 6 + .../public/cases/containers/configure/mock.ts | 9 ++ .../servicenow/helpers.ts | 12 ++ .../servicenow_itsm_params.test.tsx | 106 +++++++++---- .../servicenow/servicenow_itsm_params.tsx | 91 +++++++++-- .../servicenow/servicenow_sir_params.tsx | 16 +- .../builtin_action_types/servicenow/types.ts | 2 - .../uptime/public/state/api/alert_actions.ts | 2 + .../server/servicenow_simulation.ts | 2 + .../builtin_action_types/servicenow.ts | 2 + .../basic/tests/cases/push_case.ts | 32 +++- .../user_actions/get_all_user_actions.ts | 8 +- .../basic/tests/connectors/case.ts | 2 + 36 files changed, 666 insertions(+), 129 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/helpers.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.ts diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 1d50bc7e05807..9d48e618b76dc 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -595,6 +595,8 @@ The following table describes the properties of the `incident` object. | severity | The name of the severity in ServiceNow. | string _(optional)_ | | urgency | The name of the urgency in ServiceNow. | string _(optional)_ | | impact | The name of the impact in ServiceNow. | string _(optional)_ | +| category | The name of the category in ServiceNow. | string _(optional)_ | +| subcategory | The name of the subcategory in ServiceNow. | string _(optional)_ | #### `subActionParams (getFields)` 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 662b1ce46a07b..8d24e48d4d515 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 @@ -88,6 +88,8 @@ describe('api', () => { severity: '1', urgency: '2', impact: '3', + category: 'software', + subcategory: 'os', caller_id: 'elastic', description: 'Incident description', short_description: 'Incident title', @@ -111,6 +113,8 @@ describe('api', () => { severity: '1', urgency: '2', impact: '3', + category: 'software', + subcategory: 'os', comments: 'A comment', description: 'Incident description', short_description: 'Incident title', @@ -123,6 +127,8 @@ describe('api', () => { severity: '1', urgency: '2', impact: '3', + category: 'software', + subcategory: 'os', comments: 'Another comment', description: 'Incident description', short_description: 'Incident title', @@ -146,6 +152,8 @@ describe('api', () => { severity: '1', urgency: '2', impact: '3', + category: 'software', + subcategory: 'os', work_notes: 'A comment', description: 'Incident description', short_description: 'Incident title', @@ -158,6 +166,8 @@ describe('api', () => { severity: '1', urgency: '2', impact: '3', + category: 'software', + subcategory: 'os', work_notes: 'Another comment', description: 'Incident description', short_description: 'Incident title', @@ -229,6 +239,8 @@ describe('api', () => { severity: '1', urgency: '2', impact: '3', + category: 'software', + subcategory: 'os', description: 'Incident description', short_description: 'Incident title', }, @@ -251,6 +263,8 @@ describe('api', () => { severity: '1', urgency: '2', impact: '3', + category: 'software', + subcategory: 'os', description: 'Incident description', short_description: 'Incident title', }, @@ -262,6 +276,8 @@ describe('api', () => { severity: '1', urgency: '2', impact: '3', + category: 'software', + subcategory: 'os', comments: 'A comment', description: 'Incident description', short_description: 'Incident title', @@ -285,6 +301,8 @@ describe('api', () => { severity: '1', urgency: '2', impact: '3', + category: 'software', + subcategory: 'os', description: 'Incident description', short_description: 'Incident title', }, @@ -296,6 +314,8 @@ describe('api', () => { severity: '1', urgency: '2', impact: '3', + category: 'software', + subcategory: 'os', work_notes: 'A comment', description: 'Incident description', short_description: 'Incident title', 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 8a689bffb3408..909200472be33 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 @@ -112,6 +112,8 @@ const executorParams: ExecutorSubActionPushParams = { severity: '1', urgency: '2', impact: '3', + category: 'software', + subcategory: 'os', }, comments: [ { 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 b89d53ee2c66e..59b0803d189cd 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts @@ -45,6 +45,8 @@ const CommonAttributes = { short_description: schema.string(), description: schema.nullable(schema.string()), externalId: schema.nullable(schema.string()), + category: schema.nullable(schema.string()), + subcategory: schema.nullable(schema.string()), }; // Schema for ServiceNow Incident Management (ITSM) @@ -62,13 +64,11 @@ export const ExecutorSubActionPushParamsSchemaITSM = schema.object({ export const ExecutorSubActionPushParamsSchemaSIR = schema.object({ incident: schema.object({ ...CommonAttributes, - category: schema.nullable(schema.string()), dest_ip: schema.nullable(schema.string()), malware_hash: schema.nullable(schema.string()), malware_url: schema.nullable(schema.string()), - priority: schema.nullable(schema.string()), source_ip: schema.nullable(schema.string()), - subcategory: schema.nullable(schema.string()), + priority: schema.nullable(schema.string()), }), comments: CommentsSchema, }); diff --git a/x-pack/plugins/case/common/api/connectors/jira.ts b/x-pack/plugins/case/common/api/connectors/jira.ts index 15a6768b07561..d61f4ba91575e 100644 --- a/x-pack/plugins/case/common/api/connectors/jira.ts +++ b/x-pack/plugins/case/common/api/connectors/jira.ts @@ -7,6 +7,7 @@ import * as rt from 'io-ts'; +// New fields should also be added at: x-pack/plugins/case/server/connectors/case/schema.ts export const JiraFieldsRT = rt.type({ issueType: rt.union([rt.string, rt.null]), priority: rt.union([rt.string, rt.null]), diff --git a/x-pack/plugins/case/common/api/connectors/resilient.ts b/x-pack/plugins/case/common/api/connectors/resilient.ts index d19aa5b21fb52..dc59588d1e6ed 100644 --- a/x-pack/plugins/case/common/api/connectors/resilient.ts +++ b/x-pack/plugins/case/common/api/connectors/resilient.ts @@ -7,6 +7,7 @@ import * as rt from 'io-ts'; +// New fields should also be added at: x-pack/plugins/case/server/connectors/case/schema.ts export const ResilientFieldsRT = rt.type({ incidentTypes: rt.union([rt.array(rt.string), rt.null]), severityCode: rt.union([rt.string, rt.null]), diff --git a/x-pack/plugins/case/common/api/connectors/servicenow_itsm.ts b/x-pack/plugins/case/common/api/connectors/servicenow_itsm.ts index 2e86a26971aaa..9eedbcb44907a 100644 --- a/x-pack/plugins/case/common/api/connectors/servicenow_itsm.ts +++ b/x-pack/plugins/case/common/api/connectors/servicenow_itsm.ts @@ -7,10 +7,13 @@ import * as rt from 'io-ts'; +// New fields should also be added at: x-pack/plugins/case/server/connectors/case/schema.ts export const ServiceNowITSMFieldsRT = rt.type({ impact: rt.union([rt.string, rt.null]), severity: rt.union([rt.string, rt.null]), urgency: rt.union([rt.string, rt.null]), + category: rt.union([rt.string, rt.null]), + subcategory: rt.union([rt.string, rt.null]), }); export type ServiceNowITSMFieldsType = rt.TypeOf; diff --git a/x-pack/plugins/case/common/api/connectors/servicenow_sir.ts b/x-pack/plugins/case/common/api/connectors/servicenow_sir.ts index 749abdea87437..b8d33f259ade7 100644 --- a/x-pack/plugins/case/common/api/connectors/servicenow_sir.ts +++ b/x-pack/plugins/case/common/api/connectors/servicenow_sir.ts @@ -7,6 +7,7 @@ import * as rt from 'io-ts'; +// New fields should also be added at: x-pack/plugins/case/server/connectors/case/schema.ts export const ServiceNowSIRFieldsRT = rt.type({ category: rt.union([rt.string, rt.null]), destIp: rt.union([rt.boolean, rt.null]), diff --git a/x-pack/plugins/case/server/connectors/case/index.test.ts b/x-pack/plugins/case/server/connectors/case/index.test.ts index 236927967d0c5..4a025fd980fe2 100644 --- a/x-pack/plugins/case/server/connectors/case/index.test.ts +++ b/x-pack/plugins/case/server/connectors/case/index.test.ts @@ -153,6 +153,8 @@ describe('case connector', () => { impact: 'Medium', severity: 'Medium', urgency: 'Medium', + category: 'software', + subcategory: 'os', }, }, settings: { @@ -218,7 +220,13 @@ describe('case connector', () => { id: 'servicenow', name: 'Servicenow', type: '.servicenow', - fields: { impact: null, severity: null, urgency: null }, + fields: { + impact: null, + severity: null, + urgency: null, + category: null, + subcategory: null, + }, }, settings: { syncAlerts: true, @@ -293,6 +301,8 @@ describe('case connector', () => { impact: 'Medium', severity: 'Medium', urgency: 'Medium', + category: 'software', + subcategory: 'os', excess: null, }, }, @@ -470,6 +480,8 @@ describe('case connector', () => { impact: 'Medium', severity: 'Medium', urgency: 'Medium', + category: 'software', + subcategory: 'os', }, }, }, @@ -517,7 +529,13 @@ describe('case connector', () => { id: 'servicenow', name: 'Servicenow', type: '.servicenow', - fields: { impact: null, severity: null, urgency: null }, + fields: { + impact: null, + severity: null, + urgency: null, + category: null, + subcategory: null, + }, }, }, }); @@ -590,6 +608,8 @@ describe('case connector', () => { impact: 'Medium', severity: 'Medium', urgency: 'Medium', + category: 'software', + subcategory: 'os', excess: null, }, }, diff --git a/x-pack/plugins/case/server/connectors/case/schema.ts b/x-pack/plugins/case/server/connectors/case/schema.ts index ba82190367b12..8d52a344308e1 100644 --- a/x-pack/plugins/case/server/connectors/case/schema.ts +++ b/x-pack/plugins/case/server/connectors/case/schema.ts @@ -53,6 +53,8 @@ const ServiceNowFieldsSchema = schema.object({ impact: schema.nullable(schema.string()), severity: schema.nullable(schema.string()), urgency: schema.nullable(schema.string()), + category: schema.nullable(schema.string()), + subcategory: schema.nullable(schema.string()), }); const NoneFieldsSchema = schema.nullable(schema.object({})); diff --git a/x-pack/plugins/case/server/connectors/servicenow/itsm_formatter.ts b/x-pack/plugins/case/server/connectors/servicenow/itsm_formatter.ts index 60faa82a9e3fa..b49eed6a4ad26 100644 --- a/x-pack/plugins/case/server/connectors/servicenow/itsm_formatter.ts +++ b/x-pack/plugins/case/server/connectors/servicenow/itsm_formatter.ts @@ -9,9 +9,9 @@ import { ServiceNowITSMFieldsType, ConnectorServiceNowITSMTypeFields } from '../ import { ExternalServiceFormatter } from '../types'; const format: ExternalServiceFormatter['format'] = (theCase) => { - const { severity = null, urgency = null, impact = null } = + const { severity = null, urgency = null, impact = null, category = null, subcategory = null } = (theCase.connector.fields as ConnectorServiceNowITSMTypeFields['fields']) ?? {}; - return { severity, urgency, impact }; + return { severity, urgency, impact, category, subcategory }; }; export const serviceNowITSMExternalServiceFormatter: ExternalServiceFormatter = { diff --git a/x-pack/plugins/case/server/connectors/servicenow/itsm_formmater.test.ts b/x-pack/plugins/case/server/connectors/servicenow/itsm_formmater.test.ts index 033f184c7e751..ea3a4e41e17b8 100644 --- a/x-pack/plugins/case/server/connectors/servicenow/itsm_formmater.test.ts +++ b/x-pack/plugins/case/server/connectors/servicenow/itsm_formmater.test.ts @@ -10,7 +10,9 @@ import { serviceNowITSMExternalServiceFormatter } from './itsm_formatter'; describe('ITSM formatter', () => { const theCase = { - connector: { fields: { severity: '2', urgency: '2', impact: '2' } }, + connector: { + fields: { severity: '2', urgency: '2', impact: '2', category: 'software', subcategory: 'os' }, + }, } as CaseResponse; it('it formats correctly', async () => { @@ -21,6 +23,12 @@ describe('ITSM formatter', () => { it('it formats correctly when fields do not exist ', async () => { const invalidFields = { connector: { fields: null } } as CaseResponse; const res = await serviceNowITSMExternalServiceFormatter.format(invalidFields, []); - expect(res).toEqual({ severity: null, urgency: null, impact: null }); + expect(res).toEqual({ + severity: null, + urgency: null, + impact: null, + category: null, + subcategory: null, + }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/objects/case.ts b/x-pack/plugins/security_solution/cypress/objects/case.ts index 7a3ce2cb00dfa..a0135431c6543 100644 --- a/x-pack/plugins/security_solution/cypress/objects/case.ts +++ b/x-pack/plugins/security_solution/cypress/objects/case.ts @@ -153,6 +153,18 @@ export const executeResponses = { value: 'inbound_ddos', element: 'subcategory', }, + { + dependent_value: '', + label: 'Software', + value: 'software', + element: 'category', + }, + { + dependent_value: 'software', + label: 'Operation System', + value: 'os', + element: 'subcategory', + }, ...['severity', 'urgency', 'impact', 'priority'] .map((element) => [ { diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts index 15dddd48f0aca..e67cee4f38734 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts @@ -92,6 +92,6 @@ export const fillIbmResilientConnectorOptions = ( ibmResilientConnector.incidentTypes.forEach((incidentType) => { cy.get(SELECT_INCIDENT_TYPE).type(`${incidentType}{enter}`, { force: true }); }); - cy.get(CONNECTOR_RESILIENT).click(); + cy.get(CONNECTOR_RESILIENT).click({ force: true }); cy.get(SELECT_SEVERITY).select(ibmResilientConnector.severity); }; diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/mock.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/mock.ts index 04e7338025258..f5429fa2396aa 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/mock.ts @@ -58,6 +58,18 @@ export const choices = [ value: 'inbound_ddos', element: 'subcategory', }, + { + dependent_value: '', + label: 'Software', + value: 'software', + element: 'category', + }, + { + dependent_value: 'software', + label: 'Operation System', + value: 'os', + element: 'subcategory', + }, ...['severity', 'urgency', 'impact', 'priority'] .map((element) => [ { diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/helpers.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/helpers.ts new file mode 100644 index 0000000000000..314d224491128 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/helpers.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiSelectOption } from '@elastic/eui'; +import { Choice } from './types'; + +export const choicesToEuiOptions = (choices: Choice[]): EuiSelectOption[] => + choices.map((choice) => ({ value: choice.value, text: choice.label })); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx index 555ed0dcbb161..6e2bdec360fdf 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx @@ -20,12 +20,18 @@ jest.mock('../../../../common/lib/kibana'); jest.mock('./use_get_choices', () => ({ useGetChoices: (args: { onSuccess: () => void }) => { onChoicesSuccess = args.onSuccess; - return { isLoading: false, mockChoices }; + return { isLoading: false, choices: mockChoices }; }, })); describe('ServiceNowITSM Fields', () => { - const fields = { severity: '1', urgency: '2', impact: '3' }; + const fields = { + severity: '1', + urgency: '2', + impact: '3', + category: 'software', + subcategory: 'os', + }; const onChange = jest.fn(); beforeEach(() => { @@ -37,6 +43,8 @@ describe('ServiceNowITSM Fields', () => { expect(wrapper.find('[data-test-subj="severitySelect"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="urgencySelect"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="impactSelect"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="categorySelect"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="subcategorySelect"]').exists()).toBeTruthy(); }); it('all params fields are rendered - isEdit: false', () => { @@ -58,6 +66,42 @@ describe('ServiceNowITSM Fields', () => { ); }); + test('it transforms the categories to options correctly', async () => { + const wrapper = mount(); + act(() => { + onChoicesSuccess(mockChoices); + }); + + wrapper.update(); + expect(wrapper.find('[data-test-subj="categorySelect"]').first().prop('options')).toEqual([ + { value: 'Priviledge Escalation', text: 'Priviledge Escalation' }, + { + value: 'Criminal activity/investigation', + text: 'Criminal activity/investigation', + }, + { value: 'Denial of Service', text: 'Denial of Service' }, + { + value: 'software', + text: 'Software', + }, + ]); + }); + + test('it transforms the subcategories to options correctly', async () => { + const wrapper = mount(); + act(() => { + onChoicesSuccess(mockChoices); + }); + + wrapper.update(); + expect(wrapper.find('[data-test-subj="subcategorySelect"]').first().prop('options')).toEqual([ + { + text: 'Operation System', + value: 'os', + }, + ]); + }); + it('it transforms the options correctly', async () => { const wrapper = mount(); act(() => { @@ -81,7 +125,7 @@ describe('ServiceNowITSM Fields', () => { expect(onChange).toHaveBeenCalledWith(fields); - const testers = ['severity', 'urgency', 'impact']; + const testers = ['severity', 'urgency', 'impact', 'subcategory']; testers.forEach((subj) => test(`${subj.toUpperCase()}`, async () => { await waitFor(() => { @@ -99,5 +143,22 @@ describe('ServiceNowITSM Fields', () => { }); }) ); + + test('it should set subcategory to null when changing category', async () => { + await waitFor(() => { + const select = wrapper.find(EuiSelect).filter(`[data-test-subj="categorySelect"]`)!; + select.prop('onChange')!({ + target: { + value: 'network', + }, + } as React.ChangeEvent); + }); + wrapper.update(); + expect(onChange).toHaveBeenCalledWith({ + ...fields, + subcategory: null, + category: 'network', + }); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_itsm_case_fields.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_itsm_case_fields.tsx index e278492b57148..1fe592cfdebc4 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_itsm_case_fields.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_itsm_case_fields.tsx @@ -17,22 +17,39 @@ import { import { useKibana } from '../../../../common/lib/kibana'; import { ConnectorCard } from '../card'; import { useGetChoices } from './use_get_choices'; -import { Options, Choice } from './types'; +import { Fields, Choice } from './types'; +import { choicesToEuiOptions } from './helpers'; -const useGetChoicesFields = ['urgency', 'severity', 'impact']; -const defaultOptions: Options = { +const useGetChoicesFields = ['urgency', 'severity', 'impact', 'category', 'subcategory']; +const defaultFields: Fields = { urgency: [], severity: [], impact: [], + category: [], + subcategory: [], }; const ServiceNowITSMFieldsComponent: React.FunctionComponent< ConnectorFieldsProps > = ({ isEdit = true, fields, connector, onChange }) => { const init = useRef(true); - const { severity = null, urgency = null, impact = null } = fields ?? {}; + const { severity = null, urgency = null, impact = null, category = null, subcategory = null } = + fields ?? {}; const { http, notifications } = useKibana().services; - const [options, setOptions] = useState(defaultOptions); + const [choices, setChoices] = useState(defaultFields); + + const categoryOptions = useMemo(() => choicesToEuiOptions(choices.category), [choices.category]); + const urgencyOptions = useMemo(() => choicesToEuiOptions(choices.urgency), [choices.urgency]); + const severityOptions = useMemo(() => choicesToEuiOptions(choices.severity), [choices.severity]); + const impactOptions = useMemo(() => choicesToEuiOptions(choices.impact), [choices.impact]); + + const subcategoryOptions = useMemo( + () => + choicesToEuiOptions( + choices.subcategory.filter((choice) => choice.dependent_value === category) + ), + [choices.subcategory, category] + ); const listItems = useMemo( () => [ @@ -40,7 +57,7 @@ const ServiceNowITSMFieldsComponent: React.FunctionComponent< ? [ { title: i18n.URGENCY, - description: options.urgency.find((option) => `${option.value}` === urgency)?.text, + description: urgencyOptions.find((option) => `${option.value}` === urgency)?.text, }, ] : []), @@ -48,7 +65,7 @@ const ServiceNowITSMFieldsComponent: React.FunctionComponent< ? [ { title: i18n.SEVERITY, - description: options.severity.find((option) => `${option.value}` === severity)?.text, + description: severityOptions.find((option) => `${option.value}` === severity)?.text, }, ] : []), @@ -56,27 +73,53 @@ const ServiceNowITSMFieldsComponent: React.FunctionComponent< ? [ { title: i18n.IMPACT, - description: options.impact.find((option) => `${option.value}` === impact)?.text, + description: impactOptions.find((option) => `${option.value}` === impact)?.text, + }, + ] + : []), + ...(category != null && category.length > 0 + ? [ + { + title: i18n.CATEGORY, + description: categoryOptions.find((option) => `${option.value}` === category)?.text, + }, + ] + : []), + ...(subcategory != null && subcategory.length > 0 + ? [ + { + title: i18n.SUBCATEGORY, + description: subcategoryOptions.find((option) => `${option.value}` === subcategory) + ?.text, }, ] : []), ], - [urgency, options.urgency, options.severity, options.impact, severity, impact] + [ + category, + categoryOptions, + impact, + impactOptions, + severity, + severityOptions, + subcategory, + subcategoryOptions, + urgency, + urgencyOptions, + ] ); - const onChoicesSuccess = (choices: Choice[]) => - setOptions( - choices.reduce( - (acc, choice) => ({ + const onChoicesSuccess = (values: Choice[]) => { + setChoices( + values.reduce( + (acc, value) => ({ ...acc, - [choice.element]: [ - ...(acc[choice.element] != null ? acc[choice.element] : []), - { value: choice.value, text: choice.label }, - ], + [value.element]: [...(acc[value.element] != null ? acc[value.element] : []), value], }), - defaultOptions + defaultFields ) ); + }; const { isLoading: isLoadingChoices } = useGetChoices({ http, @@ -100,17 +143,17 @@ const ServiceNowITSMFieldsComponent: React.FunctionComponent< useEffect(() => { if (init.current) { init.current = false; - onChange({ urgency, severity, impact }); + onChange({ urgency, severity, impact, category, subcategory }); } - }, [impact, onChange, severity, urgency]); + }, [category, impact, onChange, severity, subcategory, urgency]); return isEdit ? ( -
+
+ + + + onChange({ ...fields, category: e.target.value, subcategory: null })} + /> + + + + + onChangeCb('subcategory', e.target.value)} + /> + + +
) : ( { text: 'Criminal activity/investigation', }, { value: 'Denial of Service', text: 'Denial of Service' }, + { + text: 'Software', + value: 'software', + }, ]); }); @@ -176,7 +180,7 @@ describe('ServiceNowSIR Fields', () => { }) ); - const testers = ['priority', 'category', 'subcategory']; + const testers = ['priority', 'subcategory']; testers.forEach((subj) => test(`${subj.toUpperCase()}`, async () => { await waitFor(() => { @@ -194,5 +198,24 @@ describe('ServiceNowSIR Fields', () => { }); }) ); + + test('it should set subcategory to null when changing category', async () => { + const select = wrapper.find(EuiSelect).filter(`[data-test-subj="categorySelect"]`)!; + select.prop('onChange')!({ + target: { + value: 'network', + }, + } as React.ChangeEvent); + + wrapper.update(); + + await waitFor(() => { + expect(onChange).toHaveBeenCalledWith({ + ...fields, + subcategory: null, + category: 'network', + }); + }); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_sir_case_fields.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_sir_case_fields.tsx index 96db43fe261ac..68cb4f867b334 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_sir_case_fields.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_sir_case_fields.tsx @@ -6,14 +6,7 @@ */ import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; -import { - EuiFormRow, - EuiSelect, - EuiFlexGroup, - EuiFlexItem, - EuiSelectOption, - EuiCheckbox, -} from '@elastic/eui'; +import { EuiFormRow, EuiSelect, EuiFlexGroup, EuiFlexItem, EuiCheckbox } from '@elastic/eui'; import { ConnectorTypes, @@ -24,6 +17,7 @@ import { ConnectorFieldsProps } from '../types'; import { ConnectorCard } from '../card'; import { useGetChoices } from './use_get_choices'; import { Choice, Fields } from './types'; +import { choicesToEuiOptions } from './helpers'; import * as i18n from './translations'; @@ -34,9 +28,6 @@ const defaultFields: Fields = { priority: [], }; -const choicesToEuiOptions = (choices: Choice[]): EuiSelectOption[] => - choices.map((choice) => ({ value: choice.value, text: choice.label })); - const ServiceNowSIRFieldsComponent: React.FunctionComponent< ConnectorFieldsProps > = ({ isEdit = true, fields, connector, onChange }) => { @@ -179,7 +170,7 @@ const ServiceNowSIRFieldsComponent: React.FunctionComponent< }, [category, destIp, malwareHash, malwareUrl, onChange, priority, sourceIp, subcategory]); return isEdit ? ( -
+
@@ -259,7 +250,7 @@ const ServiceNowSIRFieldsComponent: React.FunctionComponent< isLoading={isLoadingChoices} disabled={isLoadingChoices} hasNoInitialSelection - onChange={(e) => onChangeCb('category', e.target.value)} + onChange={(e) => onChange({ ...fields, category: e.target.value, subcategory: null })} /> @@ -269,7 +260,8 @@ const ServiceNowSIRFieldsComponent: React.FunctionComponent< fullWidth data-test-subj="subcategorySelect" options={subcategoryOptions} - value={subcategory ?? undefined} + // Needs an empty string instead of undefined to select the blank option when changing categories + value={subcategory ?? ''} isLoading={isLoadingChoices} disabled={isLoadingChoices} hasNoInitialSelection diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/types.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/types.ts index deceeed29482b..fd1af62f7bb2a 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/types.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/types.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { EuiSelectOption } from '@elastic/eui'; - export interface Choice { value: string; label: string; @@ -15,4 +13,3 @@ export interface Choice { } export type Fields = Record; -export type Options = Record; diff --git a/x-pack/plugins/security_solution/public/cases/components/create/connector.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/connector.test.tsx index 236c13e5afc08..9c5a4a0784af1 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/connector.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/connector.test.tsx @@ -96,10 +96,10 @@ describe('Connector', () => { ); }); - // await waitFor(() => { - // wrapper.update(); - // expect(wrapper.find(`[data-test-subj="connector-fields-sn"]`).exists()).toBeTruthy(); - // }); + await waitFor(() => { + wrapper.update(); + expect(wrapper.find(`[data-test-subj="connector-fields-sn-itsm"]`).exists()).toBeTruthy(); + }); }); it('it is loading when fetching connectors', async () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/create/form_context.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/form_context.test.tsx index 87658a78ac6f7..8236ab7b19d27 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/form_context.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/form_context.test.tsx @@ -20,6 +20,7 @@ import { connectorsMock } from '../../containers/configure/mock'; import { useGetIncidentTypes } from '../connectors/resilient/use_get_incident_types'; import { useGetSeverity } from '../connectors/resilient/use_get_severity'; import { useGetIssueTypes } from '../connectors/jira/use_get_issue_types'; +import { useGetChoices } from '../connectors/servicenow/use_get_choices'; import { useGetFieldsByIssueType } from '../connectors/jira/use_get_fields_by_issue_type'; import { useCaseConfigureResponse } from '../configure_cases/__mock__'; import { @@ -30,6 +31,7 @@ import { useGetSeverityResponse, useGetIssueTypesResponse, useGetFieldsByIssueTypeResponse, + useGetChoicesResponse, } from './mock'; import { FormContext } from './form_context'; import { CreateCaseForm } from './form'; @@ -49,6 +51,7 @@ jest.mock('../connectors/jira/use_get_issue_types'); jest.mock('../connectors/jira/use_get_fields_by_issue_type'); jest.mock('../connectors/jira/use_get_single_issue'); jest.mock('../connectors/jira/use_get_issues'); +jest.mock('../connectors/servicenow/use_get_choices'); const useConnectorsMock = useConnectors as jest.Mock; const useCaseConfigureMock = useCaseConfigure as jest.Mock; @@ -58,6 +61,7 @@ const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock; const useGetSeverityMock = useGetSeverity as jest.Mock; const useGetIssueTypesMock = useGetIssueTypes as jest.Mock; const useGetFieldsByIssueTypeMock = useGetFieldsByIssueType as jest.Mock; +const useGetChoicesMock = useGetChoices as jest.Mock; const postCase = jest.fn(); const pushCaseToExternalService = jest.fn(); @@ -109,6 +113,7 @@ describe('Create case', () => { useGetSeverityMock.mockReturnValue(useGetSeverityResponse); useGetIssueTypesMock.mockReturnValue(useGetIssueTypesResponse); useGetFieldsByIssueTypeMock.mockReturnValue(useGetFieldsByIssueTypeResponse); + useGetChoicesMock.mockReturnValue(useGetChoicesResponse); (useGetTags as jest.Mock).mockImplementation(() => ({ tags: sampleTags, @@ -219,6 +224,8 @@ describe('Create case', () => { impact: null, severity: null, urgency: null, + category: null, + subcategory: null, }, id: 'servicenow-1', name: 'My Connector', @@ -399,7 +406,7 @@ describe('Create case', () => { }); }); - it(`it should submit and push to servicenow connector`, async () => { + it(`it should submit and push to servicenow itsm connector`, async () => { useConnectorsMock.mockReturnValue({ ...sampleConnectorData, connectors: connectorsMock, @@ -415,10 +422,14 @@ describe('Create case', () => { ); fillForm(wrapper); - expect(wrapper.find(`[data-test-subj="connector-fields-sn"]`).exists()).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="connector-fields-sn-itsm"]`).exists()).toBeFalsy(); wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); wrapper.find(`button[data-test-subj="dropdown-connector-servicenow-1"]`).simulate('click'); - expect(wrapper.find(`[data-test-subj="connector-fields-sn"]`).exists()).toBeTruthy(); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find(`[data-test-subj="connector-fields-sn-itsm"]`).exists()).toBeTruthy(); + }); ['severitySelect', 'urgencySelect', 'impactSelect'].forEach((subj) => { wrapper @@ -429,6 +440,20 @@ describe('Create case', () => { }); }); + wrapper + .find('select[data-test-subj="categorySelect"]') + .first() + .simulate('change', { + target: { value: 'software' }, + }); + + wrapper + .find('select[data-test-subj="subcategorySelect"]') + .first() + .simulate('change', { + target: { value: 'os' }, + }); + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); await waitFor(() => { @@ -438,7 +463,13 @@ describe('Create case', () => { id: 'servicenow-1', name: 'My Connector', type: '.servicenow', - fields: { impact: '2', severity: '2', urgency: '2' }, + fields: { + impact: '2', + severity: '2', + urgency: '2', + category: 'software', + subcategory: 'os', + }, }, }); @@ -448,7 +479,110 @@ describe('Create case', () => { id: 'servicenow-1', name: 'My Connector', type: '.servicenow', - fields: { impact: '2', severity: '2', urgency: '2' }, + fields: { + impact: '2', + severity: '2', + urgency: '2', + category: 'software', + subcategory: 'os', + }, + }, + }); + + expect(onFormSubmitSuccess).toHaveBeenCalledWith({ + id: sampleId, + ...sampleData, + }); + }); + }); + + it(`it should submit and push to servicenow sir connector`, async () => { + useConnectorsMock.mockReturnValue({ + ...sampleConnectorData, + connectors: connectorsMock, + }); + + const wrapper = mount( + + + + + + + ); + + fillForm(wrapper); + expect(wrapper.find(`[data-test-subj="connector-fields-sn-sir"]`).exists()).toBeFalsy(); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.find(`button[data-test-subj="dropdown-connector-servicenow-sir"]`).simulate('click'); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find(`[data-test-subj="connector-fields-sn-sir"]`).exists()).toBeTruthy(); + }); + + wrapper + .find('[data-test-subj="destIpCheckbox"] input') + .first() + .simulate('change', { target: { checked: false } }); + + wrapper + .find('select[data-test-subj="prioritySelect"]') + .first() + .simulate('change', { + target: { value: '1' }, + }); + + wrapper + .find('select[data-test-subj="categorySelect"]') + .first() + .simulate('change', { + target: { value: 'Denial of Service' }, + }); + + wrapper + .find('select[data-test-subj="subcategorySelect"]') + .first() + .simulate('change', { + target: { value: '26' }, + }); + + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + + await waitFor(() => { + expect(postCase).toBeCalledWith({ + ...sampleData, + connector: { + id: 'servicenow-sir', + name: 'My Connector SIR', + type: '.servicenow-sir', + fields: { + destIp: false, + sourceIp: true, + malwareHash: true, + malwareUrl: true, + priority: '1', + category: 'Denial of Service', + subcategory: '26', + }, + }, + }); + + expect(pushCaseToExternalService).toHaveBeenCalledWith({ + caseId: sampleId, + connector: { + id: 'servicenow-sir', + name: 'My Connector SIR', + type: '.servicenow-sir', + fields: { + destIp: false, + sourceIp: true, + malwareHash: true, + malwareUrl: true, + priority: '1', + category: 'Denial of Service', + subcategory: '26', + }, }, }); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/mock.ts b/x-pack/plugins/security_solution/public/cases/components/create/mock.ts index 5044b859702fa..909b49940e189 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/components/create/mock.ts @@ -7,6 +7,7 @@ import { CasePostRequest } from '../../../../../case/common/api'; import { ConnectorTypes } from '../../../../../case/common/api/connectors'; +import { choices } from '../connectors/mock'; export const sampleTags = ['coke', 'pepsi']; export const sampleData: CasePostRequest = { @@ -93,3 +94,8 @@ export const useGetFieldsByIssueTypeResponse = { }, }, }; + +export const useGetChoicesResponse = { + isLoading: false, + choices, +}; diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts b/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts index 283e55f3759c7..c4ae60c7d1a73 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts @@ -61,6 +61,15 @@ export const connectorsMock: ActionConnector[] = [ }, isPreconfigured: false, }, + { + id: 'servicenow-sir', + actionTypeId: '.servicenow-sir', + name: 'My Connector SIR', + config: { + apiUrl: 'https://instance1.service-now.com', + }, + isPreconfigured: false, + }, ]; export const actionTypesMock: ActionTypeConnector[] = [ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.ts new file mode 100644 index 0000000000000..314d224491128 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiSelectOption } from '@elastic/eui'; +import { Choice } from './types'; + +export const choicesToEuiOptions = (choices: Choice[]): EuiSelectOption[] => + choices.map((choice) => ({ value: choice.value, text: choice.label })); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.test.tsx index bfc32ef67e46f..e864a8d3fd114 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.test.tsx @@ -28,6 +28,8 @@ const actionParams = { severity: '1', urgency: '2', impact: '3', + category: 'software', + subcategory: 'os', externalId: null, }, comments: [], @@ -55,34 +57,48 @@ const defaultProps = { const useGetChoicesResponse = { isLoading: false, - choices: ['severity', 'urgency', 'impact'] - .map((element) => [ - { - dependent_value: '', - label: '1 - Critical', - value: '1', - element, - }, - { - dependent_value: '', - label: '2 - High', - value: '2', - element, - }, - { - dependent_value: '', - label: '3 - Moderate', - value: '3', - element, - }, - { - dependent_value: '', - label: '4 - Low', - value: '4', - element, - }, - ]) - .flat(), + choices: [ + { + dependent_value: '', + label: 'Software', + value: 'software', + element: 'category', + }, + { + dependent_value: 'software', + label: 'Operation System', + value: 'os', + element: 'subcategory', + }, + ...['severity', 'urgency', 'impact'] + .map((element) => [ + { + dependent_value: '', + label: '1 - Critical', + value: '1', + element, + }, + { + dependent_value: '', + label: '2 - High', + value: '2', + element, + }, + { + dependent_value: '', + label: '3 - Moderate', + value: '3', + element, + }, + { + dependent_value: '', + label: '4 - Low', + value: '4', + element, + }, + ]) + .flat(), + ], }; describe('ServiceNowITSMParamsFields renders', () => { @@ -101,6 +117,8 @@ describe('ServiceNowITSMParamsFields renders', () => { expect(wrapper.find('[data-test-subj="urgencySelect"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="severitySelect"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="impactSelect"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="categorySelect"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="subcategorySelect"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="short_descriptionInput"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="descriptionTextArea"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="commentsTextArea"]').exists()).toBeTruthy(); @@ -153,6 +171,36 @@ describe('ServiceNowITSMParamsFields renders', () => { }); }); + test('it transforms the categories to options correctly', async () => { + const wrapper = mount(); + act(() => { + onChoices(useGetChoicesResponse.choices); + }); + + wrapper.update(); + expect(wrapper.find('[data-test-subj="categorySelect"]').first().prop('options')).toEqual([ + { + value: 'software', + text: 'Software', + }, + ]); + }); + + test('it transforms the subcategories to options correctly', async () => { + const wrapper = mount(); + act(() => { + onChoices(useGetChoicesResponse.choices); + }); + + wrapper.update(); + expect(wrapper.find('[data-test-subj="subcategorySelect"]').first().prop('options')).toEqual([ + { + text: 'Operation System', + value: 'os', + }, + ]); + }); + test('it transforms the options correctly', async () => { const wrapper = mount(); act(() => { @@ -179,6 +227,8 @@ describe('ServiceNowITSMParamsFields renders', () => { { dataTestSubj: '[data-test-subj="urgencySelect"]', key: 'urgency' }, { dataTestSubj: '[data-test-subj="severitySelect"]', key: 'severity' }, { dataTestSubj: '[data-test-subj="impactSelect"]', key: 'impact' }, + { dataTestSubj: '[data-test-subj="categorySelect"]', key: 'category' }, + { dataTestSubj: '[data-test-subj="subcategorySelect"]', key: 'subcategory' }, ]; simpleFields.forEach((field) => diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx index 3befa232e5b52..84326a7ae9be8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx @@ -16,17 +16,22 @@ import { } from '@elastic/eui'; import { useKibana } from '../../../../common/lib/kibana'; import { ActionParamsProps } from '../../../../types'; -import { ServiceNowITSMActionParams, Choice, Options } from './types'; +import { ServiceNowITSMActionParams, Choice, Fields } from './types'; import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; import { useGetChoices } from './use_get_choices'; +import { choicesToEuiOptions } from './helpers'; + import * as i18n from './translations'; -const useGetChoicesFields = ['urgency', 'severity', 'impact']; -const defaultOptions: Options = { +const useGetChoicesFields = ['urgency', 'severity', 'impact', 'category', 'subcategory']; +const defaultFields: Fields = { + category: [], + subcategory: [], urgency: [], severity: [], impact: [], + priority: [], }; const ServiceNowParamsFields: React.FunctionComponent< @@ -48,7 +53,7 @@ const ServiceNowParamsFields: React.FunctionComponent< [actionParams.subActionParams] ); - const [options, setOptions] = useState(defaultOptions); + const [choices, setChoices] = useState(defaultFields); const editSubActionProperty = useCallback( (key: string, value: any) => { @@ -73,19 +78,32 @@ const ServiceNowParamsFields: React.FunctionComponent< [editSubActionProperty] ); - const onChoicesSuccess = (choices: Choice[]) => - setOptions( - choices.reduce( - (acc, choice) => ({ + const onChoicesSuccess = useCallback((values: Choice[]) => { + setChoices( + values.reduce( + (acc, value) => ({ ...acc, - [choice.element]: [ - ...(acc[choice.element] != null ? acc[choice.element] : []), - { value: choice.value, text: choice.label }, - ], + [value.element]: [...(acc[value.element] != null ? acc[value.element] : []), value], }), - defaultOptions + defaultFields ) ); + }, []); + + const categoryOptions = useMemo(() => choicesToEuiOptions(choices.category), [choices.category]); + const urgencyOptions = useMemo(() => choicesToEuiOptions(choices.urgency), [choices.urgency]); + const severityOptions = useMemo(() => choicesToEuiOptions(choices.severity), [choices.severity]); + const impactOptions = useMemo(() => choicesToEuiOptions(choices.impact), [choices.impact]); + + const subcategoryOptions = useMemo( + () => + choicesToEuiOptions( + choices.subcategory.filter( + (subcategory) => subcategory.dependent_value === incident.category + ) + ), + [choices.subcategory, incident.category] + ); const { isLoading: isLoadingChoices } = useGetChoices({ http, @@ -140,7 +158,7 @@ const ServiceNowParamsFields: React.FunctionComponent< hasNoInitialSelection isLoading={isLoadingChoices} disabled={isLoadingChoices} - options={options.urgency} + options={urgencyOptions} value={incident.urgency ?? ''} onChange={(e) => editSubActionProperty('urgency', e.target.value)} /> @@ -155,7 +173,7 @@ const ServiceNowParamsFields: React.FunctionComponent< hasNoInitialSelection isLoading={isLoadingChoices} disabled={isLoadingChoices} - options={options.severity} + options={severityOptions} value={incident.severity ?? ''} onChange={(e) => editSubActionProperty('severity', e.target.value)} /> @@ -169,7 +187,7 @@ const ServiceNowParamsFields: React.FunctionComponent< hasNoInitialSelection isLoading={isLoadingChoices} disabled={isLoadingChoices} - options={options.impact} + options={impactOptions} value={incident.impact ?? ''} onChange={(e) => editSubActionProperty('impact', e.target.value)} /> @@ -177,6 +195,47 @@ const ServiceNowParamsFields: React.FunctionComponent< + + + + { + editAction( + 'subActionParams', + { + incident: { ...incident, category: e.target.value, subcategory: null }, + comments, + }, + index + ); + }} + /> + + + + + editSubActionProperty('subcategory', e.target.value)} + /> + + + + - choices.map((choice) => ({ value: choice.value, text: choice.label })); - const ServiceNowSIRParamsFields: React.FunctionComponent< ActionParamsProps > = ({ actionConnector, actionParams, editAction, index, errors, messageVariables }) => { @@ -218,16 +215,7 @@ const ServiceNowSIRParamsFields: React.FunctionComponent< disabled={isLoadingChoices} options={priorityOptions} value={incident.priority ?? undefined} - onChange={(e) => { - editAction( - 'subActionParams', - { - incident: { ...incident, priority: e.target.value }, - comments, - }, - index - ); - }} + onChange={(e) => editSubActionProperty('priority', e.target.value)} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts index 09f27c92e8082..f252f4648e670 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { EuiSelectOption } from '@elastic/eui'; import { UserConfiguredActionConnector } from '../../../../types'; import { ExecutorSubActionPushParamsITSM, @@ -45,4 +44,3 @@ export interface Choice { } export type Fields = Record; -export type Options = Record; diff --git a/x-pack/plugins/uptime/public/state/api/alert_actions.ts b/x-pack/plugins/uptime/public/state/api/alert_actions.ts index 3fce0499c4501..17b3354b666c4 100644 --- a/x-pack/plugins/uptime/public/state/api/alert_actions.ts +++ b/x-pack/plugins/uptime/public/state/api/alert_actions.ts @@ -119,6 +119,8 @@ function getServiceNowActionParams(): ServiceNowActionParams { impact: '2', severity: '2', urgency: '2', + category: null, + subcategory: null, externalId: null, }, comments: [], diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/servicenow_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/servicenow_simulation.ts index e2cbd3628d5fa..8b8eb46989787 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/servicenow_simulation.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/servicenow_simulation.ts @@ -30,6 +30,8 @@ export function initPlugin(router: IRouter, path: string) { severity: schema.string({ defaultValue: '1' }), urgency: schema.string({ defaultValue: '1' }), impact: schema.string({ defaultValue: '1' }), + category: schema.maybe(schema.string()), + subcategory: schema.maybe(schema.string()), }), }, }, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts index 2d49c409a18fc..2d584f764e5e4 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts @@ -40,6 +40,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) { severity: '1', short_description: 'a title', urgency: '1', + category: 'software', + subcategory: 'software', }, comments: [ { diff --git a/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts b/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts index ef7c57b3b4749..735c079c7b850 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts @@ -80,7 +80,13 @@ export default ({ getService }: FtrProviderContext): void => { id: connector.id, name: connector.name, type: connector.actionTypeId, - fields: { urgency: '2', impact: '2', severity: '2' }, + fields: { + urgency: '2', + impact: '2', + severity: '2', + category: 'software', + subcategory: 'os', + }, }).connector, }) .expect(200); @@ -143,7 +149,13 @@ export default ({ getService }: FtrProviderContext): void => { id: connector.id, name: connector.name, type: connector.actionTypeId, - fields: { urgency: '2', impact: '2', severity: '2' }, + fields: { + urgency: '2', + impact: '2', + severity: '2', + category: 'software', + subcategory: 'os', + }, }).connector, }) .expect(200); @@ -196,7 +208,13 @@ export default ({ getService }: FtrProviderContext): void => { id: connector.id, name: connector.name, type: connector.actionTypeId, - fields: { urgency: '2', impact: '2', severity: '2' }, + fields: { + urgency: '2', + impact: '2', + severity: '2', + category: 'software', + subcategory: 'os', + }, }).connector, }) .expect(200); @@ -268,7 +286,13 @@ export default ({ getService }: FtrProviderContext): void => { id: connector.id, name: connector.name, type: connector.actionTypeId, - fields: { urgency: '2', impact: '2', severity: '2' }, + fields: { + urgency: '2', + impact: '2', + severity: '2', + category: 'software', + subcategory: 'os', + }, }).connector, }) .expect(200); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts index d83d87da1e7af..1cbf79cb3326c 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts @@ -359,7 +359,13 @@ export default ({ getService }: FtrProviderContext): void => { id: connector.id, name: connector.name, type: connector.actionTypeId, - fields: { urgency: '2', impact: '2', severity: '2' }, + fields: { + urgency: '2', + impact: '2', + severity: '2', + category: 'software', + subcategory: 'os', + }, }).connector, }) .expect(200); diff --git a/x-pack/test/case_api_integration/basic/tests/connectors/case.ts b/x-pack/test/case_api_integration/basic/tests/connectors/case.ts index bb94c31c220d6..302c3a0423bed 100644 --- a/x-pack/test/case_api_integration/basic/tests/connectors/case.ts +++ b/x-pack/test/case_api_integration/basic/tests/connectors/case.ts @@ -476,6 +476,8 @@ export default ({ getService }: FtrProviderContext): void => { impact: null, severity: null, urgency: null, + category: null, + subcategory: null, }, }, created_by: {