Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Actions][ServiceNow] Allow to close serviceNow incident when alert resolves #171760

Merged
merged 28 commits into from Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b9575ad
create subaction, api, close incident service
js-jankisalvi Nov 16, 2023
66e64a4
update api, service arguments
js-jankisalvi Nov 20, 2023
e03299f
add close data attributes internally
js-jankisalvi Nov 22, 2023
ae5c27c
Merge remote-tracking branch 'upstream/main' into servicenow-recover-…
js-jankisalvi Nov 22, 2023
b56614c
add unit tests for closeIncident service and API
js-jankisalvi Nov 22, 2023
c61847c
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Nov 22, 2023
ca1edd7
update UI
js-jankisalvi Nov 24, 2023
bfea86f
fix api integration test, add integration test for closeIncident
js-jankisalvi Nov 27, 2023
9dea10e
cleanup
js-jankisalvi Nov 27, 2023
64630fb
Merge branch 'main' into servicenow-recover-action
js-jankisalvi Nov 27, 2023
a46435b
Merge branch 'main' into servicenow-recover-action
js-jankisalvi Nov 27, 2023
d627b86
PR feedback 1
js-jankisalvi Nov 28, 2023
ce990a0
Merge remote-tracking branch 'upstream/main' into servicenow-recover-…
js-jankisalvi Nov 29, 2023
48cc417
PR feedabck, throw warning instead of error when incident not found
js-jankisalvi Nov 29, 2023
761e4c2
Merge branch 'main' into servicenow-recover-action
js-jankisalvi Nov 29, 2023
b7b7585
updated integration test
js-jankisalvi Nov 29, 2023
6cdc701
cleanup
js-jankisalvi Nov 29, 2023
e890e55
PR feedback 3
js-jankisalvi Nov 30, 2023
2cdac29
log warning if no incident found with getIncident
js-jankisalvi Nov 30, 2023
c8ff4fe
Merge branch 'main' into servicenow-recover-action
js-jankisalvi Nov 30, 2023
d163bc1
return null after warning
js-jankisalvi Nov 30, 2023
c6b81fa
Merge branch 'servicenow-recover-action' of https://github.com/js-jan…
js-jankisalvi Nov 30, 2023
7088a16
update 404 error response check
js-jankisalvi Dec 1, 2023
7fc2b4e
Merge branch 'main' into servicenow-recover-action
js-jankisalvi Dec 1, 2023
e3b43c7
add close_notes param to API, fix bug with state
js-jankisalvi Dec 1, 2023
6c2d3da
fix AxiosError mock
js-jankisalvi Dec 1, 2023
c500f7f
update default close notes
js-jankisalvi Dec 1, 2023
c2ce8fb
remove close_notes from schema and api params
js-jankisalvi Dec 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion x-pack/plugins/alerting/common/disabled_action_groups.ts
Expand Up @@ -8,7 +8,7 @@
import { RecoveredActionGroup } from './builtin_action_groups';

const DisabledActionGroupsByActionType: Record<string, string[]> = {
[RecoveredActionGroup.id]: ['.jira', '.servicenow', '.resilient'],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To allow Recovered option in Run when dropdown

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@elastic/response-ops-execution FYI, it seems that some connectors are missing from this list like the .servicenow-secops connector.

[RecoveredActionGroup.id]: ['.jira', '.resilient'],
};

export const DisabledActionTypeIdsForActionGroup: Map<string, string[]> = new Map(
Expand Down
Expand Up @@ -16,6 +16,8 @@ import { AppInfo, Choice, RESTApiError } from './types';

export const DEFAULT_CORRELATION_ID = '{{rule.id}}:{{alert.id}}';

export const ACTION_GROUP_RECOVERED = 'recovered';

export const choicesToEuiOptions = (choices: Choice[]): EuiSelectOption[] =>
choices.map((choice) => ({ value: choice.value, text: choice.label }));

Expand Down
Expand Up @@ -56,6 +56,13 @@ export const TITLE_REQUIRED = i18n.translate(
}
);

export const CORRELATION_ID_REQUIRED = i18n.translate(
'xpack.stackConnectors.components.serviceNow.requiredCorrelationIdTextField',
{
defaultMessage: 'Correlation id is required.',
}
);

export const INCIDENT = i18n.translate('xpack.stackConnectors.components.serviceNow.title', {
defaultMessage: 'Incident',
});
Expand Down
Expand Up @@ -35,7 +35,25 @@ describe('servicenow action params validation', () => {
};

expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
errors: { ['subActionParams.incident.short_description']: [] },
errors: {
['subActionParams.incident.correlation_id']: [],
['subActionParams.incident.short_description']: [],
},
});
});

test(`${SERVICENOW_ITSM_CONNECTOR_TYPE_ID}: action params validation succeeds for closeIncident subAction`, async () => {
const connectorTypeModel = connectorTypeRegistry.get(SERVICENOW_ITSM_CONNECTOR_TYPE_ID);
const actionParams = {
subAction: 'closeIncident',
subActionParams: { incident: { correlation_id: '{{test}}{{rule_id}}' } },
};

expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
errors: {
['subActionParams.incident.correlation_id']: [],
['subActionParams.incident.short_description']: [],
},
});
});

Expand All @@ -47,8 +65,24 @@ describe('servicenow action params validation', () => {

expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
errors: {
['subActionParams.incident.correlation_id']: [],
['subActionParams.incident.short_description']: ['Short description is required.'],
},
});
});

test(`${SERVICENOW_ITSM_CONNECTOR_TYPE_ID}: params validation fails when correlation_id is not valid and subAction is closeIncident`, async () => {
const connectorTypeModel = connectorTypeRegistry.get(SERVICENOW_ITSM_CONNECTOR_TYPE_ID);
const actionParams = {
subAction: 'closeIncident',
subActionParams: { incident: { correlation_id: '' } },
};

expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
errors: {
['subActionParams.incident.correlation_id']: ['Correlation id is required.'],
['subActionParams.incident.short_description']: [],
},
});
});
});
Expand Up @@ -13,7 +13,11 @@ import type {
} from '@kbn/triggers-actions-ui-plugin/public';
import { ServiceNowConfig, ServiceNowSecrets } from '../lib/servicenow/types';
import { ServiceNowITSMActionParams } from './types';
import { getConnectorDescriptiveTitle, getSelectedConnectorIcon } from '../lib/servicenow/helpers';
import {
DEFAULT_CORRELATION_ID,
getConnectorDescriptiveTitle,
getSelectedConnectorIcon,
} from '../lib/servicenow/helpers';

export const SERVICENOW_ITSM_DESC = i18n.translate(
'xpack.stackConnectors.components.serviceNowITSM.selectMessageText',
Expand Down Expand Up @@ -46,23 +50,47 @@ export function getServiceNowITSMConnectorType(): ConnectorTypeModel<
const translations = await import('../lib/servicenow/translations');
const errors = {
'subActionParams.incident.short_description': new Array<string>(),
'subActionParams.incident.correlation_id': new Array<string>(),
};
const validationResult = {
errors,
};
if (
actionParams.subActionParams &&
actionParams.subActionParams.incident &&
actionParams.subAction !== 'closeIncident' &&
js-jankisalvi marked this conversation as resolved.
Show resolved Hide resolved
!actionParams.subActionParams.incident.short_description?.length
) {
errors['subActionParams.incident.short_description'].push(translations.TITLE_REQUIRED);
}

if (
actionParams.subAction === 'closeIncident' &&
!actionParams?.subActionParams?.incident?.correlation_id?.length
) {
errors['subActionParams.incident.correlation_id'].push(
translations.CORRELATION_ID_REQUIRED
);
}
return validationResult;
},
actionParamsFields: lazy(() => import('./servicenow_itsm_params')),
customConnectorSelectItem: {
getText: getConnectorDescriptiveTitle,
getComponent: getSelectedConnectorIcon,
},
defaultActionParams: {
subAction: 'pushToService',
subActionParams: {
incident: { correlation_id: DEFAULT_CORRELATION_ID },
comments: [],
},
},
defaultRecoveredActionParams: {
subAction: 'closeIncident',
subActionParams: {
incident: { correlation_id: DEFAULT_CORRELATION_ID },
},
},
};
}
js-jankisalvi marked this conversation as resolved.
Show resolved Hide resolved
Expand Up @@ -14,6 +14,7 @@ import { useGetChoices } from '../lib/servicenow/use_get_choices';
import ServiceNowITSMParamsFields from './servicenow_itsm_params';
import { Choice } from '../lib/servicenow/types';
import { merge } from 'lodash';
import { ACTION_GROUP_RECOVERED } from '../lib/servicenow/helpers';

jest.mock('../lib/servicenow/use_get_choices');
jest.mock('@kbn/triggers-actions-ui-plugin/public/common/lib/kibana');
Expand Down Expand Up @@ -151,14 +152,11 @@ describe('ServiceNowITSMParamsFields renders', () => {
expect(title.prop('isInvalid')).toBeTruthy();
});

test('When subActionParams is undefined, set to default', () => {
const { subActionParams, ...newParams } = actionParams;

const newProps = {
...defaultProps,
actionParams: newParams,
};
mountWithIntl(<ServiceNowITSMParamsFields {...newProps} />);
test('Resets fields when connector changes', () => {
const wrapper = mountWithIntl(<ServiceNowITSMParamsFields {...defaultProps} />);
expect(editAction.mock.calls.length).toEqual(0);
wrapper.setProps({ actionConnector: { ...connector, id: '1234' } });
expect(editAction.mock.calls.length).toEqual(1);
expect(editAction.mock.calls[0][1]).toEqual({
incident: {
correlation_id: '{{rule.id}}:{{alert.id}}',
Expand All @@ -167,27 +165,17 @@ describe('ServiceNowITSMParamsFields renders', () => {
});
});

test('When subAction is undefined, set to default', () => {
const { subAction, ...newParams } = actionParams;

test('Resets fields when connector changes and action group is recovered', () => {
const newProps = {
...defaultProps,
actionParams: newParams,
selectedActionGroupId: ACTION_GROUP_RECOVERED,
};
mountWithIntl(<ServiceNowITSMParamsFields {...newProps} />);
expect(editAction.mock.calls[0][1]).toEqual('pushToService');
});

test('Resets fields when connector changes', () => {
const wrapper = mountWithIntl(<ServiceNowITSMParamsFields {...defaultProps} />);
const wrapper = mountWithIntl(<ServiceNowITSMParamsFields {...newProps} />);
expect(editAction.mock.calls.length).toEqual(0);
wrapper.setProps({ actionConnector: { ...connector, id: '1234' } });
expect(editAction.mock.calls.length).toEqual(1);
expect(editAction.mock.calls[0][1]).toEqual({
incident: {
correlation_id: '{{rule.id}}:{{alert.id}}',
},
comments: [],
incident: { correlation_id: '{{rule.id}}:{{alert.id}}' },
});
});

Expand Down Expand Up @@ -299,5 +287,58 @@ describe('ServiceNowITSMParamsFields renders', () => {
expect(comments.simulate('change', changeEvent));
expect(editAction.mock.calls[0][1].comments.length).toEqual(1);
});

test('shows only correlation_id field when actionGroup is recovered', () => {
const newProps = {
...defaultProps,
selectedActionGroupId: 'recovered',
};
const wrapper = mountWithIntl(<ServiceNowITSMParamsFields {...newProps} />);
expect(wrapper.find('input[data-test-subj="correlation_idInput"]').exists()).toBeTruthy();
expect(wrapper.find('input[data-test-subj="short_descriptionInput"]').exists()).toBeFalsy();
});

test('A short description change triggers editAction', () => {
const wrapper = mountWithIntl(
<ServiceNowITSMParamsFields
actionParams={{}}
errors={{ ['subActionParams.incident.short_description']: [] }}
editAction={editAction}
index={0}
/>
);

const shortDescriptionField = wrapper.find('input[data-test-subj="short_descriptionInput"]');
shortDescriptionField.simulate('change', {
target: { value: 'new updated short description' },
});

expect(editAction.mock.calls[0][1]).toEqual({
incident: { short_description: 'new updated short description' },
comments: [],
});
});

test('A correlation_id field change triggers edit action correctly when actionGroup is recovered', () => {
const wrapper = mountWithIntl(
<ServiceNowITSMParamsFields
selectedActionGroupId={'recovered'}
actionParams={{}}
errors={{ ['subActionParams.incident.short_description']: [] }}
editAction={editAction}
index={0}
/>
);
const correlationIdField = wrapper.find('input[data-test-subj="correlation_idInput"]');

correlationIdField.simulate('change', {
target: { value: 'updated correlation id' },
});

expect(editAction.mock.calls[0][1]).toEqual({
incident: { correlation_id: 'updated correlation id' },
comments: [],
js-jankisalvi marked this conversation as resolved.
Show resolved Hide resolved
});
});
});
});