Skip to content

Commit

Permalink
[SIEM] [Case] Service Now Kibana Action (#53890) (#54960)
Browse files Browse the repository at this point in the history
  • Loading branch information
stephmilovic authored Jan 16, 2020
1 parent 81fcabf commit a1b3f73
Show file tree
Hide file tree
Showing 9 changed files with 900 additions and 24 deletions.
18 changes: 10 additions & 8 deletions x-pack/legacy/plugins/actions/server/builtin_action_types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,28 @@ import { ActionTypeRegistry } from '../action_type_registry';
import { ActionsConfigurationUtilities } from '../actions_config';
import { Logger } from '../../../../../../src/core/server';

import { getActionType as getServerLogActionType } from './server_log';
import { getActionType as getSlackActionType } from './slack';
import { getActionType as getEmailActionType } from './email';
import { getActionType as getIndexActionType } from './es_index';
import { getActionType as getPagerDutyActionType } from './pagerduty';
import { getActionType as getServerLogActionType } from './server_log';
import { getActionType as getServiceNowActionType } from './servicenow';
import { getActionType as getSlackActionType } from './slack';
import { getActionType as getWebhookActionType } from './webhook';

export function registerBuiltInActionTypes({
logger,
actionTypeRegistry,
actionsConfigUtils: configurationUtilities,
actionTypeRegistry,
logger,
}: {
logger: Logger;
actionTypeRegistry: ActionTypeRegistry;
actionsConfigUtils: ActionsConfigurationUtilities;
actionTypeRegistry: ActionTypeRegistry;
logger: Logger;
}) {
actionTypeRegistry.register(getServerLogActionType({ logger }));
actionTypeRegistry.register(getSlackActionType({ configurationUtilities }));
actionTypeRegistry.register(getEmailActionType({ logger, configurationUtilities }));
actionTypeRegistry.register(getIndexActionType({ logger }));
actionTypeRegistry.register(getPagerDutyActionType({ logger, configurationUtilities }));
actionTypeRegistry.register(getServerLogActionType({ logger }));
actionTypeRegistry.register(getServiceNowActionType({ configurationUtilities }));
actionTypeRegistry.register(getSlackActionType({ configurationUtilities }));
actionTypeRegistry.register(getWebhookActionType({ logger, configurationUtilities }));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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 axios, { AxiosResponse } from 'axios';
import { Services } from '../../types';
import { ParamsType, SecretsType } from '../servicenow';

interface PostServiceNowOptions {
apiUrl: string;
data: ParamsType;
headers: Record<string, string>;
services?: Services;
secrets: SecretsType;
}

// post an event to serviceNow
export async function postServiceNow(options: PostServiceNowOptions): Promise<AxiosResponse> {
const { apiUrl, data, headers, secrets } = options;
const axiosOptions = {
headers,
validateStatus: () => true,
auth: secrets,
};
return axios.post(`${apiUrl}/api/now/v1/table/incident`, data, axiosOptions);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
/*
* 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.
*/

jest.mock('./lib/post_servicenow', () => ({
postServiceNow: jest.fn(),
}));

import { getActionType } from './servicenow';
import { ActionType, Services, ActionTypeExecutorOptions } from '../types';
import { validateConfig, validateSecrets, validateParams } from '../lib';
import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks';
import { postServiceNow } from './lib/post_servicenow';
import { createActionTypeRegistry } from './index.test';
import { configUtilsMock } from '../actions_config.mock';

const postServiceNowMock = postServiceNow as jest.Mock;

const ACTION_TYPE_ID = '.servicenow';

const services: Services = {
callCluster: async (path: string, opts: any) => {},
savedObjectsClient: savedObjectsClientMock.create(),
};

let actionType: ActionType;

const mockServiceNow = {
config: {
apiUrl: 'www.servicenowisinkibanaactions.com',
},
secrets: {
password: 'secret-password',
username: 'secret-username',
},
params: {
comments: 'hello cool service now incident',
short_description: 'this is a cool service now incident',
},
};

beforeAll(() => {
const { actionTypeRegistry } = createActionTypeRegistry();
actionType = actionTypeRegistry.get(ACTION_TYPE_ID);
});

describe('get()', () => {
test('should return correct action type', () => {
expect(actionType.id).toEqual(ACTION_TYPE_ID);
expect(actionType.name).toEqual('servicenow');
});
});

describe('validateConfig()', () => {
test('should validate and pass when config is valid', () => {
const { config } = mockServiceNow;
expect(validateConfig(actionType, config)).toEqual(config);
});

test('should validate and throw error when config is invalid', () => {
expect(() => {
validateConfig(actionType, { shouldNotBeHere: true });
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action type config: [apiUrl]: expected value of type [string] but got [undefined]"`
);
});

test('should validate and pass when the servicenow url is whitelisted', () => {
actionType = getActionType({
configurationUtilities: {
...configUtilsMock,
ensureWhitelistedUri: url => {
expect(url).toEqual('https://events.servicenow.com/v2/enqueue');
},
},
});

expect(
validateConfig(actionType, { apiUrl: 'https://events.servicenow.com/v2/enqueue' })
).toEqual({ apiUrl: 'https://events.servicenow.com/v2/enqueue' });
});

test('config validation returns an error if the specified URL isnt whitelisted', () => {
actionType = getActionType({
configurationUtilities: {
...configUtilsMock,
ensureWhitelistedUri: _ => {
throw new Error(`target url is not whitelisted`);
},
},
});

expect(() => {
validateConfig(actionType, { apiUrl: 'https://events.servicenow.com/v2/enqueue' });
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action type config: error configuring servicenow action: target url is not whitelisted"`
);
});
});

describe('validateSecrets()', () => {
test('should validate and pass when secrets is valid', () => {
const { secrets } = mockServiceNow;
expect(validateSecrets(actionType, secrets)).toEqual(secrets);
});

test('should validate and throw error when secrets is invalid', () => {
expect(() => {
validateSecrets(actionType, { username: false });
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action type secrets: [password]: expected value of type [string] but got [undefined]"`
);

expect(() => {
validateSecrets(actionType, { username: false, password: 'hello' });
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action type secrets: [username]: expected value of type [string] but got [boolean]"`
);
});
});

describe('validateParams()', () => {
test('should validate and pass when params is valid', () => {
const { params } = mockServiceNow;
expect(validateParams(actionType, params)).toEqual(params);
});

test('should validate and throw error when params is invalid', () => {
expect(() => {
validateParams(actionType, { eventAction: 'ackynollage' });
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action params: [short_description]: expected value of type [string] but got [undefined]"`
);
});
});

describe('execute()', () => {
beforeEach(() => {
postServiceNowMock.mockReset();
});
const { config, params, secrets } = mockServiceNow;
test('should succeed with valid params', async () => {
postServiceNowMock.mockImplementation(() => {
return { status: 201, data: 'data-here' };
});

const actionId = 'some-action-id';
const executorOptions: ActionTypeExecutorOptions = {
actionId,
config,
params,
secrets,
services,
};
const actionResponse = await actionType.executor(executorOptions);
const { apiUrl, data, headers } = postServiceNowMock.mock.calls[0][0];
expect({ apiUrl, data, headers, secrets }).toMatchInlineSnapshot(`
Object {
"apiUrl": "www.servicenowisinkibanaactions.com",
"data": Object {
"comments": "hello cool service now incident",
"short_description": "this is a cool service now incident",
},
"headers": Object {
"Accept": "application/json",
"Content-Type": "application/json",
},
"secrets": Object {
"password": "secret-password",
"username": "secret-username",
},
}
`);
expect(actionResponse).toMatchInlineSnapshot(`
Object {
"actionId": "some-action-id",
"data": "data-here",
"status": "ok",
}
`);
});

test('should fail when postServiceNow throws', async () => {
postServiceNowMock.mockImplementation(() => {
throw new Error('doing some testing');
});

const actionId = 'some-action-id';
const executorOptions: ActionTypeExecutorOptions = {
actionId,
config,
params,
secrets,
services,
};
const actionResponse = await actionType.executor(executorOptions);
expect(actionResponse).toMatchInlineSnapshot(`
Object {
"actionId": "some-action-id",
"message": "error posting servicenow event",
"serviceMessage": "doing some testing",
"status": "error",
}
`);
});

test('should fail when postServiceNow returns 429', async () => {
postServiceNowMock.mockImplementation(() => {
return { status: 429, data: 'data-here' };
});

const actionId = 'some-action-id';
const executorOptions: ActionTypeExecutorOptions = {
actionId,
config,
params,
secrets,
services,
};
const actionResponse = await actionType.executor(executorOptions);
expect(actionResponse).toMatchInlineSnapshot(`
Object {
"actionId": "some-action-id",
"message": "error posting servicenow event: http status 429, retry later",
"retry": true,
"status": "error",
}
`);
});

test('should fail when postServiceNow returns 501', async () => {
postServiceNowMock.mockImplementation(() => {
return { status: 501, data: 'data-here' };
});

const actionId = 'some-action-id';
const executorOptions: ActionTypeExecutorOptions = {
actionId,
config,
params,
secrets,
services,
};
const actionResponse = await actionType.executor(executorOptions);
expect(actionResponse).toMatchInlineSnapshot(`
Object {
"actionId": "some-action-id",
"message": "error posting servicenow event: http status 501, retry later",
"retry": true,
"status": "error",
}
`);
});

test('should fail when postServiceNow returns 418', async () => {
postServiceNowMock.mockImplementation(() => {
return { status: 418, data: 'data-here' };
});

const actionId = 'some-action-id';
const executorOptions: ActionTypeExecutorOptions = {
actionId,
config,
params,
secrets,
services,
};
const actionResponse = await actionType.executor(executorOptions);
expect(actionResponse).toMatchInlineSnapshot(`
Object {
"actionId": "some-action-id",
"message": "error posting servicenow event: unexpected status 418",
"status": "error",
}
`);
});
});
Loading

0 comments on commit a1b3f73

Please sign in to comment.