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

ServiceNow push to Incident generic implementation (supporting both Case specific and generic Alerts) #68464

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
d16c22e
Draft ServiceNow generic implementation
YulNaumenko Jun 6, 2020
53ac886
simple working servicenow incident per alert
YulNaumenko Jun 8, 2020
7c529a0
fixed running times
YulNaumenko Jun 8, 2020
cb5e3b2
rely on externalId for update incident on the next execution
YulNaumenko Jun 8, 2020
dca285e
Merge remote-tracking branch 'upstream/master' into actions-service-n…
YulNaumenko Jun 9, 2020
5f9d371
Added consumer to the action type to be able to split ServiceNow for …
YulNaumenko Jun 11, 2020
d3db873
Added subActions support for ServiceNow action form
YulNaumenko Jun 11, 2020
9ea3450
Basic version for Alerts part for ServiceNow
YulNaumenko Jun 11, 2020
ae59bcd
Keep Case ServiceNow functionality working
YulNaumenko Jun 12, 2020
49b1fa5
Merge remote-tracking branch 'upstream/master' into actions-service-n…
YulNaumenko Jun 12, 2020
2a988e1
Revert changes on app_router
YulNaumenko Jun 12, 2020
7fb045a
Merge remote-tracking branch 'upstream/master' into actions-service-n…
YulNaumenko Jun 15, 2020
6a61abe
Fixed type checks
YulNaumenko Jun 15, 2020
819e002
Fixed language check issues
YulNaumenko Jun 15, 2020
ff50f73
Fixed actions unit tests
YulNaumenko Jun 15, 2020
41604df
Fixed functional tests
YulNaumenko Jun 16, 2020
752904f
Fixed jest tests
YulNaumenko Jun 16, 2020
c2836b4
fixed tests
YulNaumenko Jun 16, 2020
508c7c3
Merge remote-tracking branch 'upstream/master' into actions-service-n…
YulNaumenko Jun 16, 2020
beda51d
Copied case mappings to alerting plugin
YulNaumenko Jun 16, 2020
0e96eb9
made consumer optional
YulNaumenko Jun 17, 2020
6c8e007
Cleanup tests
YulNaumenko Jun 17, 2020
5d832f6
more cleanup
YulNaumenko Jun 17, 2020
9627b9b
Fixed jest tests and type checks
YulNaumenko Jun 17, 2020
9d330cd
fixed tests
YulNaumenko Jun 17, 2020
298d056
fixed servicenow validation tests
YulNaumenko Jun 17, 2020
18a8807
Added ServiceNow unit tests
YulNaumenko Jun 17, 2020
76c6630
Merge remote-tracking branch 'upstream/master' into actions-service-n…
YulNaumenko Jun 17, 2020
9795b71
Merge remote-tracking branch 'upstream/master' into actions-service-n…
YulNaumenko Jun 17, 2020
cd92798
Merge remote-tracking branch 'upstream/master' into actions-service-n…
YulNaumenko Jun 22, 2020
be23dd3
Merge remote-tracking branch 'upstream/master' into actions-service-n…
YulNaumenko Jun 26, 2020
cac96df
Removed consumer for actions
YulNaumenko Jun 29, 2020
8b59d18
Merge remote-tracking branch 'upstream/master' into actions-service-n…
YulNaumenko Jun 29, 2020
59fe7ac
fixed client side isCaseOwned support
YulNaumenko Jun 29, 2020
42b60da
fixed failing tests
YulNaumenko Jun 29, 2020
2ed44d7
fixed jest tests
YulNaumenko Jun 29, 2020
0ee8604
Fixed URL validation
YulNaumenko Jun 30, 2020
c0bec8d
fixed due to comments
YulNaumenko Jun 30, 2020
8757548
fixed tests
YulNaumenko Jun 30, 2020
a3d29e4
fixed jest tests
YulNaumenko Jun 30, 2020
2eafff5
Fixed due to comments. Moved ServiceNow filtering in case plugin to s…
YulNaumenko Jun 30, 2020
c2041f1
Merge remote-tracking branch 'upstream/master' into actions-service-n…
YulNaumenko Jun 30, 2020
e83f57f
fixed mock for ServiceNow
YulNaumenko Jun 30, 2020
9d971e2
Merge remote-tracking branch 'upstream/master' into actions-service-n…
YulNaumenko Jun 30, 2020
c74e791
Merge master
YulNaumenko Jul 1, 2020
df1f47d
Merge remote-tracking branch 'upstream/master' into actions-service-n…
YulNaumenko Jul 2, 2020
f769478
fixed consumer config
YulNaumenko Jul 3, 2020
38846b0
Merge remote-tracking branch 'upstream/master' into actions-service-n…
YulNaumenko Jul 5, 2020
31a991b
fixed test
YulNaumenko Jul 5, 2020
f333f42
fixed type check
YulNaumenko Jul 6, 2020
eddbd87
Fixed jest test
YulNaumenko Jul 6, 2020
591b9b6
fixed type check
YulNaumenko Jul 6, 2020
f44e4e8
Merge master
YulNaumenko Jul 6, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ two out-of-the box connectors: <<slack-action-type, Slack>> and <<webhook-action
my-slack1: <1>
actionTypeId: .slack <2>
name: 'Slack #xyz' <3>
secrets: <4>
secrets:
webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz'
webhook-service:
actionTypeId: .webhook
name: 'Email service'
config:
config: <4>
url: 'https://email-alert-service.elastic.co'
method: post
headers:
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/actions/server/actions_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export class ActionsClient {

this.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);

const result = await this.savedObjectsClient.update('action', id, {
const result = await this.savedObjectsClient.update<RawAction>('action', id, {
actionTypeId,
name,
config: validatedActionTypeConfig as SavedObjectAttributes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const pushToServiceHandler = async ({
}

const fields = prepareFieldsForTransformation({
params,
externalCase: params.externalCase,
mapping,
defaultPipes,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export const ExecutorSubActionSchema = schema.oneOf([
]);

export const ExecutorSubActionPushParamsSchema = schema.object({
caseId: schema.string(),
savedObjectId: schema.string(),
title: schema.string(),
description: schema.nullable(schema.string()),
comments: schema.nullable(schema.arrayOf(CommentSchema)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export interface PipedField {
}

export interface PrepareFieldsForTransformArgs {
params: PushToServiceApiParams;
externalCase: Record<string, any>;
mapping: Map<string, MapRecord>;
defaultPipes?: string[];
}
Expand Down
139 changes: 10 additions & 129 deletions x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/

import axios from 'axios';

import {
normalizeMapping,
buildMap,
mapParams,
prepareFieldsForTransformation,
transformFields,
transformComments,
addTimeZoneToDate,
throwIfNotAlive,
request,
patch,
getErrorMessage,
} from './utils';

import { SUPPORTED_SOURCE_FIELDS } from './constants';
import { Comment, MapRecord, PushToServiceApiParams } from './types';

jest.mock('axios');
const axiosMock = (axios as unknown) as jest.Mock;

const mapping: MapRecord[] = [
{ source: 'title', target: 'short_description', actionType: 'overwrite' },
{ source: 'description', target: 'description', actionType: 'append' },
Expand Down Expand Up @@ -63,7 +53,7 @@ const maliciousMapping: MapRecord[] = [
];

const fullParams: PushToServiceApiParams = {
caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa',
savedObjectId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa',
title: 'a title',
description: 'a description',
createdAt: '2020-03-13T08:34:53.450Z',
Expand Down Expand Up @@ -132,7 +122,7 @@ describe('buildMap', () => {
describe('mapParams', () => {
test('maps params correctly', () => {
const params = {
caseId: '123',
savedObjectId: '123',
incidentId: '456',
title: 'Incident title',
description: 'Incident description',
Expand All @@ -148,7 +138,7 @@ describe('mapParams', () => {

test('do not add fields not in mapping', () => {
const params = {
caseId: '123',
savedObjectId: '123',
incidentId: '456',
title: 'Incident title',
description: 'Incident description',
Expand All @@ -164,7 +154,7 @@ describe('mapParams', () => {
describe('prepareFieldsForTransformation', () => {
test('prepare fields with defaults', () => {
const res = prepareFieldsForTransformation({
params: fullParams,
externalCase: fullParams.externalCase,
mapping: finalMapping,
});
expect(res).toEqual([
Expand All @@ -185,7 +175,7 @@ describe('prepareFieldsForTransformation', () => {

test('prepare fields with default pipes', () => {
const res = prepareFieldsForTransformation({
params: fullParams,
externalCase: fullParams.externalCase,
mapping: finalMapping,
defaultPipes: ['myTestPipe'],
});
Expand All @@ -209,7 +199,7 @@ describe('prepareFieldsForTransformation', () => {
describe('transformFields', () => {
test('transform fields for creation correctly', () => {
const fields = prepareFieldsForTransformation({
params: fullParams,
externalCase: fullParams.externalCase,
mapping: finalMapping,
});

Expand All @@ -226,14 +216,7 @@ describe('transformFields', () => {

test('transform fields for update correctly', () => {
const fields = prepareFieldsForTransformation({
params: {
...fullParams,
updatedAt: '2020-03-15T08:34:53.450Z',
updatedBy: {
username: 'anotherUser',
fullName: 'Another User',
},
},
externalCase: fullParams.externalCase,
mapping: finalMapping,
defaultPipes: ['informationUpdated'],
});
Expand Down Expand Up @@ -262,7 +245,7 @@ describe('transformFields', () => {

test('add newline character to descripton', () => {
const fields = prepareFieldsForTransformation({
params: fullParams,
externalCase: fullParams.externalCase,
mapping: finalMapping,
defaultPipes: ['informationUpdated'],
});
Expand All @@ -280,7 +263,7 @@ describe('transformFields', () => {

test('append username if fullname is undefined when create', () => {
const fields = prepareFieldsForTransformation({
params: fullParams,
externalCase: fullParams.externalCase,
mapping: finalMapping,
});

Expand All @@ -300,14 +283,7 @@ describe('transformFields', () => {

test('append username if fullname is undefined when update', () => {
const fields = prepareFieldsForTransformation({
params: {
...fullParams,
updatedAt: '2020-03-15T08:34:53.450Z',
updatedBy: {
username: 'anotherUser',
fullName: 'Another User',
},
},
externalCase: fullParams.externalCase,
mapping: finalMapping,
defaultPipes: ['informationUpdated'],
});
Expand Down Expand Up @@ -479,98 +455,3 @@ describe('transformComments', () => {
]);
});
});

describe('addTimeZoneToDate', () => {
test('adds timezone with default', () => {
const date = addTimeZoneToDate('2020-04-14T15:01:55.456Z');
expect(date).toBe('2020-04-14T15:01:55.456Z GMT');
});

test('adds timezone correctly', () => {
const date = addTimeZoneToDate('2020-04-14T15:01:55.456Z', 'PST');
expect(date).toBe('2020-04-14T15:01:55.456Z PST');
});
});

describe('throwIfNotAlive ', () => {
test('throws correctly when status is invalid', async () => {
expect(() => {
throwIfNotAlive(404, 'application/json');
}).toThrow('Instance is not alive.');
});

test('throws correctly when content is invalid', () => {
expect(() => {
throwIfNotAlive(200, 'application/html');
}).toThrow('Instance is not alive.');
});

test('do NOT throws with custom validStatusCodes', async () => {
expect(() => {
throwIfNotAlive(404, 'application/json', [404]);
}).not.toThrow('Instance is not alive.');
});
});

describe('request', () => {
beforeEach(() => {
axiosMock.mockImplementation(() => ({
status: 200,
headers: { 'content-type': 'application/json' },
data: { incidentId: '123' },
}));
});

test('it fetch correctly with defaults', async () => {
const res = await request({ axios, url: '/test' });

expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'get', data: {} });
expect(res).toEqual({
status: 200,
headers: { 'content-type': 'application/json' },
data: { incidentId: '123' },
});
});

test('it fetch correctly', async () => {
const res = await request({ axios, url: '/test', method: 'post', data: { id: '123' } });

expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'post', data: { id: '123' } });
expect(res).toEqual({
status: 200,
headers: { 'content-type': 'application/json' },
data: { incidentId: '123' },
});
});

test('it throws correctly', async () => {
axiosMock.mockImplementation(() => ({
status: 404,
headers: { 'content-type': 'application/json' },
data: { incidentId: '123' },
}));

await expect(request({ axios, url: '/test' })).rejects.toThrow();
});
});

describe('patch', () => {
beforeEach(() => {
axiosMock.mockImplementation(() => ({
status: 200,
headers: { 'content-type': 'application/json' },
}));
});

test('it fetch correctly', async () => {
await patch({ axios, url: '/test', data: { id: '123' } });
expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'patch', data: { id: '123' } });
});
});

describe('getErrorMessage', () => {
test('it returns the correct error message', () => {
const msg = getErrorMessage('My connector name', 'An error has occurred');
expect(msg).toBe('[Action][My connector name]: An error has occurred');
});
});
54 changes: 3 additions & 51 deletions x-pack/plugins/actions/server/builtin_action_types/case/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import { curry, flow, get } from 'lodash';
import { schema } from '@kbn/config-schema';
import { AxiosInstance, Method, AxiosResponse } from 'axios';

import { ActionTypeExecutorOptions, ActionTypeExecutorResult, ActionType } from '../../types';

Expand Down Expand Up @@ -134,65 +133,18 @@ export const createConnector = ({
});
};

export const throwIfNotAlive = (
status: number,
contentType: string,
validStatusCodes: number[] = [200, 201, 204]
) => {
if (!validStatusCodes.includes(status) || !contentType.includes('application/json')) {
throw new Error('Instance is not alive.');
}
};

export const request = async <T = unknown>({
axios,
url,
method = 'get',
data,
}: {
axios: AxiosInstance;
url: string;
method?: Method;
data?: T;
}): Promise<AxiosResponse> => {
const res = await axios(url, { method, data: data ?? {} });
throwIfNotAlive(res.status, res.headers['content-type']);
return res;
};

export const patch = async <T = unknown>({
axios,
url,
data,
}: {
axios: AxiosInstance;
url: string;
data: T;
}): Promise<AxiosResponse> => {
return request({
axios,
url,
method: 'patch',
data,
});
};

export const addTimeZoneToDate = (date: string, timezone = 'GMT'): string => {
return `${date} ${timezone}`;
};

export const prepareFieldsForTransformation = ({
params,
externalCase,
mapping,
defaultPipes = ['informationCreated'],
}: PrepareFieldsForTransformArgs): PipedField[] => {
return Object.keys(params.externalCase)
return Object.keys(externalCase)
.filter((p) => mapping.get(p)?.actionType != null && mapping.get(p)?.actionType !== 'nothing')
.map((p) => {
const actionType = mapping.get(p)?.actionType ?? 'nothing';
return {
key: p,
value: params.externalCase[p],
value: externalCase[p],
actionType,
pipes: actionType === 'append' ? [...defaultPipes, 'append'] : defaultPipes,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ export function registerBuiltInActionTypes({
actionTypeRegistry.register(getServerLogActionType({ logger }));
actionTypeRegistry.register(getSlackActionType({ configurationUtilities }));
actionTypeRegistry.register(getWebhookActionType({ logger, configurationUtilities }));
actionTypeRegistry.register(getServiceNowActionType({ configurationUtilities }));
actionTypeRegistry.register(getServiceNowActionType({ logger, configurationUtilities }));
actionTypeRegistry.register(getJiraActionType({ configurationUtilities }));
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ mapping.set('summary', {
});

const executorParams: ExecutorSubActionPushParams = {
caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa',
savedObjectId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa',
externalId: 'incident-3',
createdAt: '2020-04-27T10:59:46.202Z',
createdBy: { fullName: 'Elastic User', username: 'elastic' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
import axios from 'axios';

import { createExternalService } from './service';
import * as utils from '../case/utils';
import * as utils from '../lib/axios_utils';
import { ExternalService } from '../case/types';

jest.mock('axios');
jest.mock('../case/utils', () => {
const originalUtils = jest.requireActual('../case/utils');
jest.mock('../lib/axios_utils', () => {
const originalUtils = jest.requireActual('../lib/axios_utils');
return {
...originalUtils,
request: jest.fn(),
Expand Down
Loading