diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/lib/send_pagerduty.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/lib/send_pagerduty.ts index 1b140a6c50a3638..dd86e32c8c7c7c0 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/lib/send_pagerduty.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/lib/send_pagerduty.ts @@ -5,16 +5,22 @@ */ import axios, { AxiosResponse } from 'axios'; +import { Services } from '../../types'; interface SendPagerdutyOptions { apiUrl: string; data: any; headers: Record; + services: Services; } // post an event to pagerduty export async function sendPagerduty(options: SendPagerdutyOptions): Promise { const { apiUrl, data, headers } = options; + const axiosOptions = { + headers, + validateStatus: () => true, + }; - return axios.post(apiUrl, data, { headers }); + return axios.post(apiUrl, data, axiosOptions); } diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.test.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.test.ts index ff20828d83f04cb..0b0badbc8eb7119 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.test.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.test.ts @@ -79,32 +79,18 @@ describe('validateConfig()', () => { }).toThrowErrorMatchingInlineSnapshot( `"error validating action type config: [shouldNotBeHere]: definition for this key is missing"` ); - expect(() => { - validateConfig(actionType, { apiUrl: false }); - }).toThrowErrorMatchingInlineSnapshot(` -"error validating action type config: [apiUrl]: types that failed validation: -- [apiUrl.0]: expected value of type [string] but got [boolean] -- [apiUrl.1]: expected value to equal [null] but got [false]" -`); }); }); describe('validateSecrets()', () => { test('should validate and pass when secrets is valid', () => { - const routingKey = '0123456789ABCDEF0123456789ABCDEF'; + const routingKey = 'super-secret'; expect(validateSecrets(actionType, { routingKey })).toEqual({ routingKey, }); }); test('should validate and throw error when secrets is invalid', () => { - const routingKey = '123456789ABCDEF0123456789ABCDEF'; // 31 chars! - expect(() => { - validateSecrets(actionType, { routingKey }); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type secrets: [routingKey]: value is [123456789ABCDEF0123456789ABCDEF] but it must have a minimum length of [32]."` - ); - expect(() => { validateSecrets(actionType, { routingKey: false }); }).toThrowErrorMatchingInlineSnapshot( @@ -166,35 +152,38 @@ describe('execute()', () => { const id = 'some-action-id'; const executorOptions: ActionTypeExecutorOptions = { id, config, params, secrets, services }; const actionResponse = await actionType.executor(executorOptions); - expect(sendPagerdutyMock.mock.calls[0][0]).toMatchInlineSnapshot(` - Object { - "apiUrl": "https://events.pagerduty.com/v2/enqueue", - "data": Object { - "dedup_key": "action:some-action-id", - "event_action": "trigger", - "payload": Object { - "severity": "info", - "source": "Kibana Action some-action-id", - "summary": "No summary provided.", - }, - }, - "headers": Object { - "Content-Type": "application/json", - "X-Routing-Key": "super-secret", - }, - } - `); + const { apiUrl, data, headers } = sendPagerdutyMock.mock.calls[0][0]; + expect({ apiUrl, data, headers }).toMatchInlineSnapshot(` + Object { + "apiUrl": "https://events.pagerduty.com/v2/enqueue", + "data": Object { + "dedup_key": "action:some-action-id", + "event_action": "trigger", + "payload": Object { + "severity": "info", + "source": "Kibana Action some-action-id", + "summary": "No summary provided.", + }, + }, + "headers": Object { + "Content-Type": "application/json", + "X-Routing-Key": "super-secret", + }, + } + `); expect(actionResponse).toMatchInlineSnapshot(` - Object { - "data": "data-here", - "status": "ok", - } - `); + Object { + "data": "data-here", + "status": "ok", + } + `); }); test('should succeed with maximal valid params for trigger', async () => { const randoDate = new Date('1963-09-23T01:23:45Z').toISOString(); - const secrets = { routingKey: 'super-secret' }; + const secrets = { + routingKey: 'super-secret', + }; const config = { apiUrl: 'the-api-url', }; @@ -217,39 +206,42 @@ describe('execute()', () => { const id = 'some-action-id'; const executorOptions: ActionTypeExecutorOptions = { id, config, params, secrets, services }; const actionResponse = await actionType.executor(executorOptions); - expect(sendPagerdutyMock.mock.calls[0][0]).toMatchInlineSnapshot(` - Object { - "apiUrl": "the-api-url", - "data": Object { - "dedup_key": "a-dedup-key", - "event_action": "trigger", - "payload": Object { - "class": "the-class", - "component": "the-component", - "group": "the-group", - "severity": "critical", - "source": "the-source", - "summary": "the summary", - "timestamp": "1963-09-23T01:23:45.000Z", - }, - }, - "headers": Object { - "Content-Type": "application/json", - "X-Routing-Key": "super-secret", - }, - } - `); + const { apiUrl, data, headers } = sendPagerdutyMock.mock.calls[0][0]; + expect({ apiUrl, data, headers }).toMatchInlineSnapshot(` + Object { + "apiUrl": "the-api-url", + "data": Object { + "dedup_key": "a-dedup-key", + "event_action": "trigger", + "payload": Object { + "class": "the-class", + "component": "the-component", + "group": "the-group", + "severity": "critical", + "source": "the-source", + "summary": "the summary", + "timestamp": "1963-09-23T01:23:45.000Z", + }, + }, + "headers": Object { + "Content-Type": "application/json", + "X-Routing-Key": "super-secret", + }, + } + `); expect(actionResponse).toMatchInlineSnapshot(` - Object { - "data": "data-here", - "status": "ok", - } - `); + Object { + "data": "data-here", + "status": "ok", + } + `); }); test('should succeed with maximal valid params for acknowledge', async () => { const randoDate = new Date('1963-09-23T01:23:45Z').toISOString(); - const secrets = { routingKey: 'super-secret' }; + const secrets = { + routingKey: 'super-secret', + }; const config = { apiUrl: 'the-api-url', }; @@ -272,30 +264,33 @@ describe('execute()', () => { const id = 'some-action-id'; const executorOptions: ActionTypeExecutorOptions = { id, config, params, secrets, services }; const actionResponse = await actionType.executor(executorOptions); - expect(sendPagerdutyMock.mock.calls[0][0]).toMatchInlineSnapshot(` - Object { - "apiUrl": "the-api-url", - "data": Object { - "dedup_key": "a-dedup-key", - "event_action": "acknowledge", - }, - "headers": Object { - "Content-Type": "application/json", - "X-Routing-Key": "super-secret", - }, - } - `); + const { apiUrl, data, headers } = sendPagerdutyMock.mock.calls[0][0]; + expect({ apiUrl, data, headers }).toMatchInlineSnapshot(` + Object { + "apiUrl": "the-api-url", + "data": Object { + "dedup_key": "a-dedup-key", + "event_action": "acknowledge", + }, + "headers": Object { + "Content-Type": "application/json", + "X-Routing-Key": "super-secret", + }, + } + `); expect(actionResponse).toMatchInlineSnapshot(` - Object { - "data": "data-here", - "status": "ok", - } - `); + Object { + "data": "data-here", + "status": "ok", + } + `); }); test('should succeed with maximal valid params for resolve', async () => { const randoDate = new Date('1963-09-23T01:23:45Z').toISOString(); - const secrets = { routingKey: 'super-secret' }; + const secrets = { + routingKey: 'super-secret', + }; const config = { apiUrl: 'the-api-url', }; @@ -318,25 +313,26 @@ describe('execute()', () => { const id = 'some-action-id'; const executorOptions: ActionTypeExecutorOptions = { id, config, params, secrets, services }; const actionResponse = await actionType.executor(executorOptions); - expect(sendPagerdutyMock.mock.calls[0][0]).toMatchInlineSnapshot(` - Object { - "apiUrl": "the-api-url", - "data": Object { - "dedup_key": "a-dedup-key", - "event_action": "resolve", - }, - "headers": Object { - "Content-Type": "application/json", - "X-Routing-Key": "super-secret", - }, - } - `); + const { apiUrl, data, headers } = sendPagerdutyMock.mock.calls[0][0]; + expect({ apiUrl, data, headers }).toMatchInlineSnapshot(` + Object { + "apiUrl": "the-api-url", + "data": Object { + "dedup_key": "a-dedup-key", + "event_action": "resolve", + }, + "headers": Object { + "Content-Type": "application/json", + "X-Routing-Key": "super-secret", + }, + } + `); expect(actionResponse).toMatchInlineSnapshot(` - Object { - "data": "data-here", - "status": "ok", - } - `); + Object { + "data": "data-here", + "status": "ok", + } + `); }); test('should fail when sendPagerdury throws', async () => { @@ -352,11 +348,11 @@ describe('execute()', () => { const executorOptions: ActionTypeExecutorOptions = { id, config, params, secrets, services }; const actionResponse = await actionType.executor(executorOptions); expect(actionResponse).toMatchInlineSnapshot(` - Object { - "message": "error in pagerduty action \\"some-action-id\\" posting event: doing some testing", - "status": "error", - } - `); + Object { + "message": "error in pagerduty action \\"some-action-id\\" posting event: doing some testing", + "status": "error", + } + `); }); test('should fail when sendPagerdury returns 429', async () => { @@ -372,12 +368,12 @@ describe('execute()', () => { const executorOptions: ActionTypeExecutorOptions = { id, config, params, secrets, services }; const actionResponse = await actionType.executor(executorOptions); expect(actionResponse).toMatchInlineSnapshot(` - Object { - "message": "error in pagerduty action \\"some-action-id\\" posting event: status 429, retry later", - "retry": true, - "status": "error", - } - `); + Object { + "message": "error in pagerduty action \\"some-action-id\\" posting event: status 429, retry later", + "retry": true, + "status": "error", + } + `); }); test('should fail when sendPagerdury returns 501', async () => { @@ -393,12 +389,12 @@ describe('execute()', () => { const executorOptions: ActionTypeExecutorOptions = { id, config, params, secrets, services }; const actionResponse = await actionType.executor(executorOptions); expect(actionResponse).toMatchInlineSnapshot(` - Object { - "message": "error in pagerduty action \\"some-action-id\\" posting event: status 501, retry later", - "retry": true, - "status": "error", - } - `); + Object { + "message": "error in pagerduty action \\"some-action-id\\" posting event: status 501, retry later", + "retry": true, + "status": "error", + } + `); }); test('should fail when sendPagerdury returns 418', async () => { @@ -414,10 +410,10 @@ describe('execute()', () => { const executorOptions: ActionTypeExecutorOptions = { id, config, params, secrets, services }; const actionResponse = await actionType.executor(executorOptions); expect(actionResponse).toMatchInlineSnapshot(` - Object { - "message": "error in pagerduty action \\"some-action-id\\" posting event: unexpected status 418", - "status": "error", - } - `); + Object { + "message": "error in pagerduty action \\"some-action-id\\" posting event: unexpected status 418", + "status": "error", + } + `); }); }); diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.ts index c8e32aa2f938ad9..b2698f51b417d4d 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.ts @@ -26,7 +26,7 @@ const ConfigSchema = schema.object({ export type ActionTypeSecretsType = TypeOf; const SecretsSchema = schema.object({ - routingKey: schema.string({ minLength: 32, maxLength: 32 }), + routingKey: schema.string(), }); // params definition @@ -114,7 +114,7 @@ async function executor(execOptions: ActionTypeExecutorOptions): Promise { + let simulatedActionId = ''; + let pagerdutySimulatorURL: string = ''; + + // need to wait for kibanaServer to settle ... + before(() => { + const kibanaServer = getService('kibanaServer'); + const kibanaUrl = kibanaServer.status && kibanaServer.status.kibanaServerUrl; + pagerdutySimulatorURL = `${kibanaUrl}${getExternalServiceSimulatorPath( + ExternalServiceSimulator.PAGERDUTY + )}`; + }); + + after(() => esArchiver.unload('empty_kibana')); + + it('should return successfully when passed valid create parameters', async () => { + const { body: createdAction } = await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + description: 'A pagerduty action', + actionTypeId: '.pagerduty', + secrets: { + routingKey: 'pager-duty-routing-key', + }, + }) + .expect(200); + + expect(createdAction).to.eql({ + id: createdAction.id, + description: 'A pagerduty action', + actionTypeId: '.pagerduty', + config: { + apiUrl: null, + }, + }); + + 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, + description: 'A pagerduty action', + actionTypeId: '.pagerduty', + config: { + apiUrl: null, + }, + }); + }); + + it('should return unsuccessfully when passed invalid create parameters', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + description: 'A pagerduty action', + actionTypeId: '.pagerduty', + secrets: {}, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type secrets: [routingKey]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should create pagerduty simulator action successfully', async () => { + const { body: createdSimulatedAction } = await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + description: 'A pagerduty simulator', + actionTypeId: '.pagerduty', + config: { + apiUrl: pagerdutySimulatorURL, + }, + secrets: { + routingKey: 'pager-duty-routing-key', + }, + }) + .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: { + summary: 'just a test', + }, + }) + .expect(200); + expect(result).to.eql({ + status: 'ok', + data: { + dedup_key: `action:${simulatedActionId}`, + message: 'Event processed', + status: 'success', + }, + }); + }); + + it('should handle a 40x pagerduty error', async () => { + const { body: result } = await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + summary: 'respond-with-418', + }, + }) + .expect(200); + expect(result.status).to.equal('error'); + expect(result.message).to.match( + /error in pagerduty action .+ posting event: unexpected status 418/ + ); + }); + + it('should handle a 429 pagerduty error', async () => { + const { body: result } = await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + summary: 'respond-with-429', + }, + }) + .expect(200); + + expect(result.status).to.equal('error'); + expect(result.message).to.match( + /error in pagerduty action .+ posting event: status 429, retry later/ + ); + expect(result.retry).to.equal(true); + }); + + it('should handle a 500 pagerduty error', async () => { + const { body: result } = await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + summary: 'respond-with-502', + }, + }) + .expect(200); + + expect(result.status).to.equal('error'); + expect(result.message).to.match( + /error in pagerduty action .+ posting event: status 502, retry later/ + ); + expect(result.retry).to.equal(true); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts index 0e830239ca74ca8..0780efc0fc9773e 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts @@ -20,5 +20,6 @@ export default function actionsTests({ loadTestFile }: FtrProviderContext) { 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')); }); }