[SIEM] [Case] Service Now Kibana Action (#53890)

This commit is contained in:
Steph Milovic 2020-01-15 13:50:38 -07:00 committed by GitHub
parent 22369c9992
commit 5ba24b8f54
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 901 additions and 25 deletions

View file

@ -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 }));
}

View file

@ -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);
}

View file

@ -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",
}
`);
});
});

View file

@ -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,
};
}

View file

@ -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

View file

@ -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));
},
});
}

View file

@ -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);
}

View file

@ -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);
});
});
}

View file

@ -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'));
});
}