[SIEM] [Case] Service Now Kibana Action (#53890)
This commit is contained in:
parent
22369c9992
commit
5ba24b8f54
|
@ -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 }));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* 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 { curry } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import {
|
||||
ActionType,
|
||||
ActionTypeExecutorOptions,
|
||||
ActionTypeExecutorResult,
|
||||
ExecutorType,
|
||||
} from '../types';
|
||||
import { ActionsConfigurationUtilities } from '../actions_config';
|
||||
import { postServiceNow } from './lib/post_servicenow';
|
||||
|
||||
// config definition
|
||||
export type ConfigType = TypeOf<typeof ConfigSchema>;
|
||||
|
||||
const ConfigSchemaProps = {
|
||||
apiUrl: schema.string(),
|
||||
};
|
||||
|
||||
const ConfigSchema = schema.object(ConfigSchemaProps);
|
||||
|
||||
function validateConfig(
|
||||
configurationUtilities: ActionsConfigurationUtilities,
|
||||
configObject: ConfigType
|
||||
) {
|
||||
if (configObject.apiUrl == null) {
|
||||
return i18n.translate('xpack.actions.builtin.servicenow.servicenowApiNullError', {
|
||||
defaultMessage: 'ServiceNow [apiUrl] is required',
|
||||
});
|
||||
}
|
||||
try {
|
||||
configurationUtilities.ensureWhitelistedUri(configObject.apiUrl);
|
||||
} catch (whitelistError) {
|
||||
return i18n.translate('xpack.actions.builtin.servicenow.servicenowApiWhitelistError', {
|
||||
defaultMessage: 'error configuring servicenow action: {message}',
|
||||
values: {
|
||||
message: whitelistError.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
// secrets definition
|
||||
export type SecretsType = TypeOf<typeof SecretsSchema>;
|
||||
const SecretsSchemaProps = {
|
||||
password: schema.string(),
|
||||
username: schema.string(),
|
||||
};
|
||||
|
||||
const SecretsSchema = schema.object(SecretsSchemaProps);
|
||||
|
||||
function validateSecrets(
|
||||
configurationUtilities: ActionsConfigurationUtilities,
|
||||
secrets: SecretsType
|
||||
) {
|
||||
if (secrets.username == null) {
|
||||
return i18n.translate('xpack.actions.builtin.servicenow.servicenowApiUserError', {
|
||||
defaultMessage: 'error configuring servicenow action: no secrets [username] provided',
|
||||
});
|
||||
}
|
||||
if (secrets.password == null) {
|
||||
return i18n.translate('xpack.actions.builtin.servicenow.servicenowApiPasswordError', {
|
||||
defaultMessage: 'error configuring servicenow action: no secrets [password] provided',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// params definition
|
||||
|
||||
export type ParamsType = TypeOf<typeof ParamsSchema>;
|
||||
|
||||
const ParamsSchema = schema.object({
|
||||
comments: schema.maybe(schema.string()),
|
||||
short_description: schema.string(),
|
||||
});
|
||||
|
||||
// action type definition
|
||||
export function getActionType({
|
||||
configurationUtilities,
|
||||
executor = serviceNowExecutor,
|
||||
}: {
|
||||
configurationUtilities: ActionsConfigurationUtilities;
|
||||
executor?: ExecutorType;
|
||||
}): ActionType {
|
||||
return {
|
||||
id: '.servicenow',
|
||||
name: 'servicenow',
|
||||
validate: {
|
||||
config: schema.object(ConfigSchemaProps, {
|
||||
validate: curry(validateConfig)(configurationUtilities),
|
||||
}),
|
||||
secrets: schema.object(SecretsSchemaProps, {
|
||||
validate: curry(validateSecrets)(configurationUtilities),
|
||||
}),
|
||||
params: ParamsSchema,
|
||||
},
|
||||
executor,
|
||||
};
|
||||
}
|
||||
|
||||
// action executor
|
||||
|
||||
async function serviceNowExecutor(
|
||||
execOptions: ActionTypeExecutorOptions
|
||||
): Promise<ActionTypeExecutorResult> {
|
||||
const actionId = execOptions.actionId;
|
||||
const config = execOptions.config as ConfigType;
|
||||
const secrets = execOptions.secrets as SecretsType;
|
||||
const params = execOptions.params as ParamsType;
|
||||
const headers = {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
let response;
|
||||
try {
|
||||
response = await postServiceNow({ apiUrl: config.apiUrl, data: params, headers, secrets });
|
||||
} catch (err) {
|
||||
const message = i18n.translate('xpack.actions.builtin.servicenow.postingErrorMessage', {
|
||||
defaultMessage: 'error posting servicenow event',
|
||||
});
|
||||
return {
|
||||
status: 'error',
|
||||
actionId,
|
||||
message,
|
||||
serviceMessage: err.message,
|
||||
};
|
||||
}
|
||||
if (response.status === 200 || response.status === 201 || response.status === 204) {
|
||||
return {
|
||||
status: 'ok',
|
||||
actionId,
|
||||
data: response.data,
|
||||
};
|
||||
}
|
||||
|
||||
if (response.status === 429 || response.status >= 500) {
|
||||
const message = i18n.translate('xpack.actions.builtin.servicenow.postingRetryErrorMessage', {
|
||||
defaultMessage: 'error posting servicenow event: http status {status}, retry later',
|
||||
values: {
|
||||
status: response.status,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
status: 'error',
|
||||
actionId,
|
||||
message,
|
||||
retry: true,
|
||||
};
|
||||
}
|
||||
|
||||
const message = i18n.translate('xpack.actions.builtin.servicenow.postingUnexpectedErrorMessage', {
|
||||
defaultMessage: 'error posting servicenow event: unexpected status {status}',
|
||||
values: {
|
||||
status: response.status,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
status: 'error',
|
||||
actionId,
|
||||
message,
|
||||
};
|
||||
}
|
|
@ -18,17 +18,18 @@ interface CreateTestConfigOptions {
|
|||
|
||||
// test.not-enabled is specifically not enabled
|
||||
const enabledActionTypes = [
|
||||
'.server-log',
|
||||
'.slack',
|
||||
'.email',
|
||||
'.index',
|
||||
'.pagerduty',
|
||||
'.server-log',
|
||||
'.servicenow',
|
||||
'.slack',
|
||||
'.webhook',
|
||||
'test.noop',
|
||||
'test.index-record',
|
||||
'test.failing',
|
||||
'test.rate-limit',
|
||||
'test.authorization',
|
||||
'test.failing',
|
||||
'test.index-record',
|
||||
'test.noop',
|
||||
'test.rate-limit',
|
||||
];
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
|
|
|
@ -6,16 +6,18 @@
|
|||
import Hapi from 'hapi';
|
||||
import { ActionType } from '../../../../../../legacy/plugins/actions';
|
||||
|
||||
import { initPlugin as initPagerduty } from './pagerduty_simulation';
|
||||
import { initPlugin as initServiceNow } from './servicenow_simulation';
|
||||
import { initPlugin as initSlack } from './slack_simulation';
|
||||
import { initPlugin as initWebhook } from './webhook_simulation';
|
||||
import { initPlugin as initPagerduty } from './pagerduty_simulation';
|
||||
|
||||
const NAME = 'actions-FTS-external-service-simulators';
|
||||
|
||||
export enum ExternalServiceSimulator {
|
||||
PAGERDUTY = 'pagerduty',
|
||||
SERVICENOW = 'servicenow',
|
||||
SLACK = 'slack',
|
||||
WEBHOOK = 'webhook',
|
||||
PAGERDUTY = 'pagerduty',
|
||||
}
|
||||
|
||||
export function getExternalServiceSimulatorPath(service: ExternalServiceSimulator): string {
|
||||
|
@ -23,9 +25,11 @@ export function getExternalServiceSimulatorPath(service: ExternalServiceSimulato
|
|||
}
|
||||
|
||||
export function getAllExternalServiceSimulatorPaths(): string[] {
|
||||
return Object.values(ExternalServiceSimulator).map(service =>
|
||||
const allPaths = Object.values(ExternalServiceSimulator).map(service =>
|
||||
getExternalServiceSimulatorPath(service)
|
||||
);
|
||||
allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v1/table/incident`);
|
||||
return allPaths;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
|
@ -67,9 +71,10 @@ export default function(kibana: any) {
|
|||
},
|
||||
});
|
||||
|
||||
initPagerduty(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.PAGERDUTY));
|
||||
initServiceNow(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW));
|
||||
initSlack(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.SLACK));
|
||||
initWebhook(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.WEBHOOK));
|
||||
initPagerduty(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.PAGERDUTY));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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 Joi from 'joi';
|
||||
import Hapi from 'hapi';
|
||||
|
||||
interface ServiceNowRequest extends Hapi.Request {
|
||||
payload: {
|
||||
comments: string;
|
||||
short_description: string;
|
||||
};
|
||||
}
|
||||
export function initPlugin(server: Hapi.Server, path: string) {
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path,
|
||||
options: {
|
||||
auth: false,
|
||||
validate: {
|
||||
options: { abortEarly: false },
|
||||
payload: Joi.object().keys({
|
||||
comments: Joi.string(),
|
||||
short_description: Joi.string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
handler: servicenowHandler,
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: `${path}/api/now/v1/table/incident`,
|
||||
options: {
|
||||
auth: false,
|
||||
validate: {
|
||||
options: { abortEarly: false },
|
||||
payload: Joi.object().keys({
|
||||
comments: Joi.string(),
|
||||
short_description: Joi.string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
handler: servicenowHandler,
|
||||
});
|
||||
}
|
||||
// ServiceNow simulator: create a servicenow action pointing here, and you can get
|
||||
// different responses based on the message posted. See the README.md for
|
||||
// more info.
|
||||
|
||||
function servicenowHandler(request: ServiceNowRequest, h: any) {
|
||||
const body = request.payload;
|
||||
const text = body && body.short_description;
|
||||
if (text == null) {
|
||||
return jsonResponse(h, 400, 'bad request to servicenow simulator');
|
||||
}
|
||||
|
||||
switch (text) {
|
||||
case 'success':
|
||||
return jsonResponse(h, 200, 'Success');
|
||||
|
||||
case 'created':
|
||||
return jsonResponse(h, 201, 'Created');
|
||||
|
||||
case 'no_text':
|
||||
return jsonResponse(h, 204, 'Success');
|
||||
|
||||
case 'invalid_payload':
|
||||
return jsonResponse(h, 400, 'Bad Request');
|
||||
|
||||
case 'unauthorized':
|
||||
return jsonResponse(h, 401, 'Unauthorized');
|
||||
|
||||
case 'forbidden':
|
||||
return jsonResponse(h, 403, 'Forbidden');
|
||||
|
||||
case 'not_found':
|
||||
return jsonResponse(h, 404, 'Not found');
|
||||
|
||||
case 'not_allowed':
|
||||
return jsonResponse(h, 405, 'Method not allowed');
|
||||
|
||||
case 'not_acceptable':
|
||||
return jsonResponse(h, 406, 'Not acceptable');
|
||||
|
||||
case 'unsupported':
|
||||
return jsonResponse(h, 415, 'Unsupported media type');
|
||||
|
||||
case 'status_500':
|
||||
return jsonResponse(h, 500, 'simulated servicenow 500 response');
|
||||
|
||||
case 'rate_limit':
|
||||
const response = {
|
||||
retry_after: 1,
|
||||
ok: false,
|
||||
error: 'rate_limited',
|
||||
};
|
||||
|
||||
return h
|
||||
.response(response)
|
||||
.type('application/json')
|
||||
.header('retry-after', '1')
|
||||
.code(429);
|
||||
}
|
||||
|
||||
return jsonResponse(h, 400, 'unknown request to servicenow simulator');
|
||||
}
|
||||
|
||||
function jsonResponse(h: any, code: number, object?: any) {
|
||||
if (object == null) {
|
||||
return h.response('').code(code);
|
||||
}
|
||||
|
||||
return h
|
||||
.response(JSON.stringify(object))
|
||||
.type('application/json')
|
||||
.code(code);
|
||||
}
|
|
@ -0,0 +1,271 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
import {
|
||||
getExternalServiceSimulatorPath,
|
||||
ExternalServiceSimulator,
|
||||
} from '../../../../common/fixtures/plugins/actions';
|
||||
|
||||
// node ../scripts/functional_test_runner.js --grep "Actions.servicenddd" --config=test/alerting_api_integration/security_and_spaces/config.ts
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function servicenowTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const mockServiceNow = {
|
||||
config: {
|
||||
apiUrl: 'www.servicenowisinkibanaactions.com',
|
||||
},
|
||||
secrets: {
|
||||
password: 'elastic',
|
||||
username: 'changeme',
|
||||
},
|
||||
params: {
|
||||
comments: 'hello cool service now incident',
|
||||
short_description: 'this is a cool service now incident',
|
||||
},
|
||||
};
|
||||
describe('servicenow', () => {
|
||||
let simulatedActionId = '';
|
||||
let servicenowSimulatorURL: string = '<could not determine kibana url>';
|
||||
|
||||
// need to wait for kibanaServer to settle ...
|
||||
before(() => {
|
||||
servicenowSimulatorURL = kibanaServer.resolveUrl(
|
||||
getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW)
|
||||
);
|
||||
});
|
||||
|
||||
after(() => esArchiver.unload('empty_kibana'));
|
||||
|
||||
it('should return 200 when creating a servicenow action successfully', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
.post('/api/action')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A servicenow action',
|
||||
actionTypeId: '.servicenow',
|
||||
config: {
|
||||
apiUrl: servicenowSimulatorURL,
|
||||
},
|
||||
secrets: mockServiceNow.secrets,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(createdAction).to.eql({
|
||||
id: createdAction.id,
|
||||
name: 'A servicenow action',
|
||||
actionTypeId: '.servicenow',
|
||||
config: {
|
||||
apiUrl: servicenowSimulatorURL,
|
||||
},
|
||||
});
|
||||
|
||||
expect(typeof createdAction.id).to.be('string');
|
||||
|
||||
const { body: fetchedAction } = await supertest
|
||||
.get(`/api/action/${createdAction.id}`)
|
||||
.expect(200);
|
||||
|
||||
expect(fetchedAction).to.eql({
|
||||
id: fetchedAction.id,
|
||||
name: 'A servicenow action',
|
||||
actionTypeId: '.servicenow',
|
||||
config: {
|
||||
apiUrl: servicenowSimulatorURL,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a servicenow action with no webhookUrl', async () => {
|
||||
await supertest
|
||||
.post('/api/action')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A servicenow action',
|
||||
actionTypeId: '.servicenow',
|
||||
config: {},
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type config: [apiUrl]: expected value of type [string] but got [undefined]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a servicenow action with a non whitelisted webhookUrl', async () => {
|
||||
await supertest
|
||||
.post('/api/action')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A servicenow action',
|
||||
actionTypeId: '.servicenow',
|
||||
config: {
|
||||
apiUrl: 'http://servicenow.mynonexistent.com',
|
||||
},
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type config: error configuring servicenow action: target url "http://servicenow.mynonexistent.com" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond with a 400 Bad Request when creating a servicenow action without secrets', async () => {
|
||||
await supertest
|
||||
.post('/api/action')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A servicenow action',
|
||||
actionTypeId: '.servicenow',
|
||||
config: {
|
||||
apiUrl: servicenowSimulatorURL,
|
||||
},
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type secrets: [password]: expected value of type [string] but got [undefined]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should create our servicenow simulator action successfully', async () => {
|
||||
const { body: createdSimulatedAction } = await supertest
|
||||
.post('/api/action')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A servicenow simulator',
|
||||
actionTypeId: '.servicenow',
|
||||
config: {
|
||||
apiUrl: servicenowSimulatorURL,
|
||||
},
|
||||
secrets: mockServiceNow.secrets,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
simulatedActionId = createdSimulatedAction.id;
|
||||
});
|
||||
|
||||
it('should handle executing with a simulated success', async () => {
|
||||
const { body: result } = await supertest
|
||||
.post(`/api/action/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
comments: 'success',
|
||||
short_description: 'success',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(result.status).to.eql('ok');
|
||||
});
|
||||
|
||||
it('should handle executing with a simulated success without comments', async () => {
|
||||
const { body: result } = await supertest
|
||||
.post(`/api/action/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
short_description: 'success',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(result.status).to.eql('ok');
|
||||
});
|
||||
|
||||
it('should handle failing with a simulated success without short_description', async () => {
|
||||
await supertest
|
||||
.post(`/api/action/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
comments: 'success',
|
||||
},
|
||||
})
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
actionId: simulatedActionId,
|
||||
status: 'error',
|
||||
retry: false,
|
||||
message:
|
||||
'error validating action params: [short_description]: expected value of type [string] but got [undefined]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle a 40x servicenow error', async () => {
|
||||
const { body: result } = await supertest
|
||||
.post(`/api/action/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
comments: 'invalid_payload',
|
||||
short_description: 'invalid_payload',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
expect(result.status).to.equal('error');
|
||||
expect(result.message).to.match(/error posting servicenow event: unexpected status 400/);
|
||||
});
|
||||
|
||||
it('should handle a 429 servicenow error', async () => {
|
||||
const { body: result } = await supertest
|
||||
.post(`/api/action/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
comments: 'rate_limit',
|
||||
short_description: 'rate_limit',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(result.status).to.equal('error');
|
||||
expect(result.message).to.equal(
|
||||
'error posting servicenow event: http status 429, retry later'
|
||||
);
|
||||
expect(result.retry).to.equal(true);
|
||||
});
|
||||
|
||||
it('should handle a 500 servicenow error', async () => {
|
||||
const { body: result } = await supertest
|
||||
.post(`/api/action/${simulatedActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
comments: 'status_500',
|
||||
short_description: 'status_500',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(result.status).to.equal('error');
|
||||
expect(result.message).to.equal(
|
||||
'error posting servicenow event: http status 500, retry later'
|
||||
);
|
||||
expect(result.retry).to.equal(true);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -9,18 +9,19 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
|||
// eslint-disable-next-line import/no-default-export
|
||||
export default function actionsTests({ loadTestFile }: FtrProviderContext) {
|
||||
describe('Actions', () => {
|
||||
loadTestFile(require.resolve('./builtin_action_types/email'));
|
||||
loadTestFile(require.resolve('./builtin_action_types/es_index'));
|
||||
loadTestFile(require.resolve('./builtin_action_types/pagerduty'));
|
||||
loadTestFile(require.resolve('./builtin_action_types/server_log'));
|
||||
loadTestFile(require.resolve('./builtin_action_types/servicenow'));
|
||||
loadTestFile(require.resolve('./builtin_action_types/slack'));
|
||||
loadTestFile(require.resolve('./builtin_action_types/webhook'));
|
||||
loadTestFile(require.resolve('./create'));
|
||||
loadTestFile(require.resolve('./delete'));
|
||||
loadTestFile(require.resolve('./execute'));
|
||||
loadTestFile(require.resolve('./find'));
|
||||
loadTestFile(require.resolve('./get'));
|
||||
loadTestFile(require.resolve('./list_action_types'));
|
||||
loadTestFile(require.resolve('./update'));
|
||||
loadTestFile(require.resolve('./execute'));
|
||||
loadTestFile(require.resolve('./builtin_action_types/server_log'));
|
||||
loadTestFile(require.resolve('./builtin_action_types/slack'));
|
||||
loadTestFile(require.resolve('./builtin_action_types/email'));
|
||||
loadTestFile(require.resolve('./builtin_action_types/es_index'));
|
||||
loadTestFile(require.resolve('./builtin_action_types/pagerduty'));
|
||||
loadTestFile(require.resolve('./builtin_action_types/webhook'));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue