diff --git a/x-pack/package.json b/x-pack/package.json index 2b52646e0f748..6fa98e4adf226 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -44,9 +44,9 @@ "@storybook/addon-storyshots": "^5.3.19", "@storybook/react": "^5.3.19", "@storybook/theming": "^5.3.19", + "@testing-library/jest-dom": "^5.8.0", "@testing-library/react": "^9.3.2", "@testing-library/react-hooks": "^3.2.1", - "@testing-library/jest-dom": "^5.8.0", "@types/angular": "^1.6.56", "@types/archiver": "^3.1.0", "@types/base64-js": "^1.2.5", @@ -72,8 +72,9 @@ "@types/gulp": "^4.0.6", "@types/hapi__wreck": "^15.0.1", "@types/he": "^1.1.1", - "@types/hoist-non-react-statics": "^3.3.1", "@types/history": "^4.7.3", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/http-proxy": "^1.17.4", "@types/jest": "^25.2.3", "@types/jest-specific-snapshot": "^0.5.4", "@types/joi": "^13.4.2", @@ -94,6 +95,7 @@ "@types/object-hash": "^1.3.0", "@types/papaparse": "^5.0.3", "@types/pngjs": "^3.3.2", + "@types/pretty-ms": "^5.0.0", "@types/prop-types": "^15.5.3", "@types/proper-lockfile": "^3.0.1", "@types/puppeteer": "^1.20.1", @@ -109,6 +111,7 @@ "@types/redux-actions": "^2.6.1", "@types/set-value": "^2.0.0", "@types/sinon": "^7.0.13", + "@types/stats-lite": "^2.2.0", "@types/styled-components": "^5.1.0", "@types/supertest": "^2.0.5", "@types/tar-fs": "^1.16.1", @@ -116,11 +119,9 @@ "@types/tinycolor2": "^1.4.1", "@types/use-resize-observer": "^6.0.0", "@types/uuid": "^3.4.4", + "@types/webpack-env": "^1.15.2", "@types/xml-crypto": "^1.4.0", "@types/xml2js": "^0.4.5", - "@types/stats-lite": "^2.2.0", - "@types/pretty-ms": "^5.0.0", - "@types/webpack-env": "^1.15.2", "@welldone-software/why-did-you-render": "^4.0.0", "abab": "^1.0.4", "autoprefixer": "^9.7.4", @@ -227,6 +228,7 @@ "@turf/circle": "6.0.1", "@turf/distance": "6.0.1", "@turf/helpers": "6.0.1", + "@types/http-proxy-agent": "^2.0.2", "angular": "^1.8.0", "angular-resource": "1.8.0", "angular-sanitize": "1.8.0", diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/types.ts b/x-pack/plugins/actions/server/builtin_action_types/case/types.ts index de96864d0b295..1030e3d9c5d8e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/types.ts @@ -9,6 +9,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { TypeOf } from '@kbn/config-schema'; +import { Logger } from '../../../../../../src/core/server'; import { ExternalIncidentServiceConfigurationSchema, @@ -122,7 +123,12 @@ export interface ExternalServiceApi { export interface CreateExternalServiceBasicArgs { api: ExternalServiceApi; - createExternalService: (credentials: ExternalServiceCredentials) => ExternalService; + createExternalService: ( + credentials: ExternalServiceCredentials, + logger: Logger, + proxySettings?: any + ) => ExternalService; + logger: Logger; } export interface CreateExternalServiceArgs extends CreateExternalServiceBasicArgs { diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts index 82dedb09c429e..d895bf386a367 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts @@ -67,6 +67,7 @@ export const mapParams = ( export const createConnectorExecutor = ({ api, createExternalService, + logger, }: CreateExternalServiceBasicArgs) => async ( execOptions: ActionTypeExecutorOptions< ExternalIncidentServiceConfiguration, @@ -83,10 +84,14 @@ export const createConnectorExecutor = ({ actionId, }; - const externalService = createExternalService({ - config, - secrets, - }); + const externalService = createExternalService( + { + config, + secrets, + }, + logger, + execOptions.proxySettings + ); if (!api[subAction]) { throw new Error('[Action][ExternalService] Unsupported subAction type.'); @@ -122,10 +127,11 @@ export const createConnector = ({ validate, createExternalService, validationSchema, + logger, }: CreateExternalServiceArgs) => { return ({ configurationUtilities, - executor = createConnectorExecutor({ api, createExternalService }), + executor = createConnectorExecutor({ api, createExternalService, logger }), }: CreateActionTypeArgs): ActionType => ({ ...config, validate: { diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts index 195f6db538ae5..62f369816d714 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts @@ -269,6 +269,7 @@ describe('execute()', () => { "message": "a message to you", "subject": "the subject", }, + "proxySettings": undefined, "routing": Object { "bcc": Array [ "jimmy@example.com", @@ -326,6 +327,7 @@ describe('execute()', () => { "message": "a message to you", "subject": "the subject", }, + "proxySettings": undefined, "routing": Object { "bcc": Array [ "jimmy@example.com", diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.ts b/x-pack/plugins/actions/server/builtin_action_types/email.ts index a51a0432a01e0..e9dc4eea5dcfc 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.ts @@ -184,6 +184,7 @@ async function executor( subject: params.subject, message: params.message, }, + proxySettings: execOptions.proxySettings, }; let result; diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.ts b/x-pack/plugins/actions/server/builtin_action_types/index.ts index 80a171cbe624d..3591e05fb3acf 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.ts @@ -31,9 +31,9 @@ export function registerBuiltInActionTypes({ actionTypeRegistry.register(getIndexActionType({ logger })); actionTypeRegistry.register(getPagerDutyActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getServerLogActionType({ logger })); - actionTypeRegistry.register(getSlackActionType({ configurationUtilities })); + actionTypeRegistry.register(getSlackActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getWebhookActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getServiceNowActionType({ logger, configurationUtilities })); - actionTypeRegistry.register(getJiraActionType({ configurationUtilities })); - actionTypeRegistry.register(getResilientActionType({ configurationUtilities })); + actionTypeRegistry.register(getJiraActionType({ logger, configurationUtilities })); + actionTypeRegistry.register(getResilientActionType({ logger, configurationUtilities })); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts index a2d7bb5930a75..66be0bad02d7b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts @@ -4,21 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Logger } from '../../../../../../src/core/server'; import { createConnector } from '../case/utils'; +import { ActionType } from '../../types'; import { api } from './api'; import { config } from './config'; import { validate } from './validators'; import { createExternalService } from './service'; import { JiraSecretConfiguration, JiraPublicConfiguration } from './schema'; +import { ActionsConfigurationUtilities } from '../../actions_config'; -export const getActionType = createConnector({ - api, - config, - validate, - createExternalService, - validationSchema: { - config: JiraPublicConfiguration, - secrets: JiraSecretConfiguration, - }, -}); +export function getActionType({ + logger, + configurationUtilities, +}: { + logger: Logger; + configurationUtilities: ActionsConfigurationUtilities; +}): ActionType { + return createConnector({ + api, + config, + validate, + createExternalService, + validationSchema: { + config: JiraPublicConfiguration, + secrets: JiraSecretConfiguration, + }, + logger, + })({ configurationUtilities }); +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts index 3de3926b7d821..547595b4c183f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts @@ -9,6 +9,9 @@ import axios from 'axios'; import { createExternalService } from './service'; import * as utils from '../lib/axios_utils'; import { ExternalService } from '../case/types'; +import { Logger } from '../../../../../../src/core/server'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +const logger = loggingSystemMock.create().get() as jest.Mocked; jest.mock('axios'); jest.mock('../lib/axios_utils', () => { @@ -26,10 +29,13 @@ describe('Jira service', () => { let service: ExternalService; beforeAll(() => { - service = createExternalService({ - config: { apiUrl: 'https://siem-kibana.atlassian.net', projectKey: 'CK' }, - secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, - }); + service = createExternalService( + { + config: { apiUrl: 'https://siem-kibana.atlassian.net', projectKey: 'CK' }, + secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, + }, + logger + ); }); beforeEach(() => { @@ -39,37 +45,49 @@ describe('Jira service', () => { describe('createExternalService', () => { test('throws without url', () => { expect(() => - createExternalService({ - config: { apiUrl: null, projectKey: 'CK' }, - secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, - }) + createExternalService( + { + config: { apiUrl: null, projectKey: 'CK' }, + secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, + }, + logger + ) ).toThrow(); }); test('throws without projectKey', () => { expect(() => - createExternalService({ - config: { apiUrl: 'test.com', projectKey: null }, - secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, - }) + createExternalService( + { + config: { apiUrl: 'test.com', projectKey: null }, + secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, + }, + logger + ) ).toThrow(); }); test('throws without username', () => { expect(() => - createExternalService({ - config: { apiUrl: 'test.com' }, - secrets: { apiToken: '', email: 'elastic@elastic.com' }, - }) + createExternalService( + { + config: { apiUrl: 'test.com' }, + secrets: { apiToken: '', email: 'elastic@elastic.com' }, + }, + logger + ) ).toThrow(); }); test('throws without password', () => { expect(() => - createExternalService({ - config: { apiUrl: 'test.com' }, - secrets: { apiToken: '', email: undefined }, - }) + createExternalService( + { + config: { apiUrl: 'test.com' }, + secrets: { apiToken: '', email: undefined }, + }, + logger + ) ).toThrow(); }); }); @@ -92,6 +110,7 @@ describe('Jira service', () => { expect(requestMock).toHaveBeenCalledWith({ axios, url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1', + logger, }); }); @@ -146,6 +165,7 @@ describe('Jira service', () => { expect(requestMock).toHaveBeenCalledWith({ axios, url: 'https://siem-kibana.atlassian.net/rest/api/2/issue', + logger, method: 'post', data: { fields: { @@ -210,6 +230,7 @@ describe('Jira service', () => { expect(requestMock).toHaveBeenCalledWith({ axios, + logger, method: 'put', url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1', data: { fields: { summary: 'title', description: 'desc' } }, @@ -272,6 +293,7 @@ describe('Jira service', () => { expect(requestMock).toHaveBeenCalledWith({ axios, + logger, method: 'post', url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1/comment', data: { body: 'comment' }, diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index 240b645c3a7dc..aec73cfb375ed 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -7,6 +7,7 @@ import axios from 'axios'; import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types'; +import { Logger } from '../../../../../../src/core/server'; import { JiraPublicConfigurationType, JiraSecretConfigurationType, @@ -17,6 +18,7 @@ import { import * as i18n from './translations'; import { request, getErrorMessage } from '../lib/axios_utils'; +import { ProxySettings } from '../../types'; const VERSION = '2'; const BASE_URL = `rest/api/${VERSION}`; @@ -25,10 +27,11 @@ const COMMENT_URL = `comment`; const VIEW_INCIDENT_URL = `browse`; -export const createExternalService = ({ - config, - secrets, -}: ExternalServiceCredentials): ExternalService => { +export const createExternalService = ( + { config, secrets }: ExternalServiceCredentials, + logger: Logger, + proxySettings?: ProxySettings +): ExternalService => { const { apiUrl: url, projectKey } = config as JiraPublicConfigurationType; const { apiToken, email } = secrets as JiraSecretConfigurationType; @@ -55,6 +58,8 @@ export const createExternalService = ({ const res = await request({ axios: axiosInstance, url: `${incidentUrl}/${id}`, + logger, + proxySettings, }); const { fields, ...rest } = res.data; @@ -75,10 +80,12 @@ export const createExternalService = ({ const res = await request({ axios: axiosInstance, url: `${incidentUrl}`, + logger, method: 'post', data: { fields: { ...incident, project: { key: projectKey }, issuetype: { name: 'Task' } }, }, + proxySettings, }); const updatedIncident = await getIncident(res.data.id); @@ -102,7 +109,9 @@ export const createExternalService = ({ axios: axiosInstance, method: 'put', url: `${incidentUrl}/${incidentId}`, + logger, data: { fields: { ...incident } }, + proxySettings, }); const updatedIncident = await getIncident(incidentId); @@ -129,7 +138,9 @@ export const createExternalService = ({ axios: axiosInstance, method: 'post', url: getCommentsURL(incidentId), + logger, data: { body: comment.comment }, + proxySettings, }); return { diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts index 4a52ae60bcdda..844aa6d2de7ed 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts @@ -5,7 +5,11 @@ */ import axios from 'axios'; -import { addTimeZoneToDate, throwIfNotAlive, request, patch, getErrorMessage } from './axios_utils'; +import HttpProxyAgent from 'http-proxy-agent'; +import { Logger } from '../../../../../../src/core/server'; +import { addTimeZoneToDate, request, patch, getErrorMessage } from './axios_utils'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +const logger = loggingSystemMock.create().get() as jest.Mocked; jest.mock('axios'); const axiosMock = (axios as unknown) as jest.Mock; @@ -21,26 +25,6 @@ describe('addTimeZoneToDate', () => { }); }); -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(() => ({ @@ -51,9 +35,22 @@ describe('request', () => { }); test('it fetch correctly with defaults', async () => { - const res = await request({ axios, url: '/test' }); + const res = await request({ + axios, + url: '/test', + logger, + }); - expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'get', data: {} }); + expect(axiosMock).toHaveBeenCalledWith('/test', { + method: 'get', + data: {}, + headers: undefined, + httpAgent: undefined, + httpsAgent: undefined, + params: undefined, + proxy: false, + validateStatus: undefined, + }); expect(res).toEqual({ status: 200, headers: { 'content-type': 'application/json' }, @@ -61,10 +58,27 @@ describe('request', () => { }); }); - test('it fetch correctly', async () => { - const res = await request({ axios, url: '/test', method: 'post', data: { id: '123' } }); + test('it have been called with proper proxy agent', async () => { + const res = await request({ + axios, + url: '/testProxy', + logger, + proxySettings: { + proxyUrl: 'http://localhost:1212', + rejectUnauthorizedCertificates: false, + }, + }); - expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'post', data: { id: '123' } }); + expect(axiosMock).toHaveBeenCalledWith('/testProxy', { + method: 'get', + data: {}, + headers: undefined, + httpAgent: new HttpProxyAgent('http://localhost:1212'), + httpsAgent: new HttpProxyAgent('http://localhost:1212'), + params: undefined, + proxy: false, + validateStatus: undefined, + }); expect(res).toEqual({ status: 200, headers: { 'content-type': 'application/json' }, @@ -72,14 +86,24 @@ describe('request', () => { }); }); - test('it throws correctly', async () => { - axiosMock.mockImplementation(() => ({ - status: 404, + test('it fetch correctly', async () => { + const res = await request({ axios, url: '/test', method: 'post', logger, data: { id: '123' } }); + + expect(axiosMock).toHaveBeenCalledWith('/test', { + method: 'post', + data: { id: '123' }, + headers: undefined, + httpAgent: undefined, + httpsAgent: undefined, + params: undefined, + proxy: false, + validateStatus: undefined, + }); + expect(res).toEqual({ + status: 200, headers: { 'content-type': 'application/json' }, data: { incidentId: '123' }, - })); - - await expect(request({ axios, url: '/test' })).rejects.toThrow(); + }); }); }); @@ -92,8 +116,17 @@ describe('patch', () => { }); test('it fetch correctly', async () => { - await patch({ axios, url: '/test', data: { id: '123' } }); - expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'patch', data: { id: '123' } }); + await patch({ axios, url: '/test', data: { id: '123' }, logger }); + expect(axiosMock).toHaveBeenCalledWith('/test', { + method: 'patch', + data: { id: '123' }, + headers: undefined, + httpAgent: undefined, + httpsAgent: undefined, + params: undefined, + proxy: false, + validateStatus: undefined, + }); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts index d527cf632bace..e26a3b686179c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts @@ -4,50 +4,68 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AxiosInstance, Method, AxiosResponse } from 'axios'; - -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.'); - } -}; +import { AxiosInstance, Method, AxiosResponse, AxiosBasicCredentials } from 'axios'; +import { Logger } from '../../../../../../src/core/server'; +import { ProxySettings } from '../../types'; +import { getProxyAgent } from './get_proxy_agent'; export const request = async ({ axios, url, + logger, method = 'get', data, params, + proxySettings, + headers, + validateStatus, + auth, }: { axios: AxiosInstance; url: string; + logger: Logger; method?: Method; data?: T; params?: unknown; + proxySettings?: ProxySettings; + headers?: Record | null; + validateStatus?: (status: number) => boolean; + auth?: AxiosBasicCredentials; }): Promise => { - const res = await axios(url, { method, data: data ?? {}, params }); - throwIfNotAlive(res.status, res.headers['content-type']); - return res; + return await axios(url, { + method, + data: data ?? {}, + params, + auth, + // use httpsAgent and embedded proxy: false, to be able to handle fail on invalid certs + httpsAgent: proxySettings ? getProxyAgent(proxySettings, logger) : undefined, + httpAgent: proxySettings ? getProxyAgent(proxySettings, logger) : undefined, + proxy: false, // the same way as it done for IncomingWebhook in + headers, + validateStatus, + }); }; export const patch = async ({ axios, url, data, + logger, + proxySettings, }: { axios: AxiosInstance; url: string; data: T; + logger: Logger; + proxySettings?: ProxySettings; }): Promise => { return request({ axios, url, + logger, method: 'patch', data, + proxySettings, }); }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.test.ts new file mode 100644 index 0000000000000..2468fab8c6ac5 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.test.ts @@ -0,0 +1,30 @@ +/* + * 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 HttpProxyAgent from 'http-proxy-agent'; +import { HttpsProxyAgent } from 'https-proxy-agent'; +import { Logger } from '../../../../../../src/core/server'; +import { getProxyAgent } from './get_proxy_agent'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +const logger = loggingSystemMock.create().get() as jest.Mocked; + +describe('getProxyAgent', () => { + test('return HttpsProxyAgent for https proxy url', () => { + const agent = getProxyAgent( + { proxyUrl: 'https://someproxyhost', rejectUnauthorizedCertificates: false }, + logger + ); + expect(agent instanceof HttpsProxyAgent).toBeTruthy(); + }); + + test('return HttpProxyAgent for http proxy url', () => { + const agent = getProxyAgent( + { proxyUrl: 'http://someproxyhost', rejectUnauthorizedCertificates: false }, + logger + ); + expect(agent instanceof HttpProxyAgent).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.ts new file mode 100644 index 0000000000000..bb4dadd3a4698 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.ts @@ -0,0 +1,31 @@ +/* + * 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 HttpProxyAgent from 'http-proxy-agent'; +import { HttpsProxyAgent } from 'https-proxy-agent'; +import { Logger } from '../../../../../../src/core/server'; +import { ProxySettings } from '../../types'; + +export function getProxyAgent( + proxySettings: ProxySettings, + logger: Logger +): HttpsProxyAgent | HttpProxyAgent { + logger.debug(`Create proxy agent for ${proxySettings.proxyUrl}.`); + + if (/^https/i.test(proxySettings.proxyUrl)) { + const proxyUrl = new URL(proxySettings.proxyUrl); + return new HttpsProxyAgent({ + host: proxyUrl.hostname, + port: Number(proxyUrl.port), + protocol: proxyUrl.protocol, + headers: proxySettings.proxyHeaders, + // do not fail on invalid certs if value is false + rejectUnauthorized: proxySettings.rejectUnauthorizedCertificates, + }); + } else { + return new HttpProxyAgent(proxySettings.proxyUrl); + } +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/post_pagerduty.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/post_pagerduty.ts index 92f88ebe0be22..d78237beb98a1 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/post_pagerduty.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/post_pagerduty.ts @@ -5,22 +5,34 @@ */ import axios, { AxiosResponse } from 'axios'; -import { Services } from '../../types'; +import { Logger } from '../../../../../../src/core/server'; +import { Services, ProxySettings } from '../../types'; +import { request } from './axios_utils'; interface PostPagerdutyOptions { apiUrl: string; data: unknown; headers: Record; services: Services; + proxySettings?: ProxySettings; } // post an event to pagerduty -export async function postPagerduty(options: PostPagerdutyOptions): Promise { - const { apiUrl, data, headers } = options; - const axiosOptions = { +export async function postPagerduty( + options: PostPagerdutyOptions, + logger: Logger +): Promise { + const { apiUrl, data, headers, proxySettings } = options; + const axiosInstance = axios.create(); + + return await request({ + axios: axiosInstance, + url: apiUrl, + method: 'post', + logger, + data, + proxySettings, headers, validateStatus: () => true, - }; - - return axios.post(apiUrl, data, axiosOptions); + }); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts index 3514bd4257b0f..8287ee944bca9 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts @@ -12,6 +12,7 @@ import { Logger } from '../../../../../../src/core/server'; import { sendEmail } from './send_email'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import nodemailer from 'nodemailer'; +import { ProxySettings } from '../../types'; const createTransportMock = nodemailer.createTransport as jest.Mock; const sendMailMockResult = { result: 'does not matter' }; @@ -63,6 +64,59 @@ describe('send_email module', () => { }); test('handles unauthenticated email using not secure host/port', async () => { + const sendEmailOptions = getSendEmailOptions( + { + transport: { + host: 'example.com', + port: 1025, + }, + }, + { + proxyUrl: 'https://example.com', + rejectUnauthorizedCertificates: false, + } + ); + delete sendEmailOptions.transport.service; + delete sendEmailOptions.transport.user; + delete sendEmailOptions.transport.password; + const result = await sendEmail(mockLogger, sendEmailOptions); + expect(result).toBe(sendMailMockResult); + expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "headers": undefined, + "host": "example.com", + "port": 1025, + "proxy": "https://example.com", + "secure": false, + "tls": Object { + "rejectUnauthorized": false, + }, + }, + ] + `); + expect(sendMailMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "bcc": Array [], + "cc": Array [ + "bob@example.com", + "robert@example.com", + ], + "from": "fred@example.com", + "html": "

a message

+ ", + "subject": "a subject", + "text": "a message", + "to": Array [ + "jim@example.com", + ], + }, + ] + `); + }); + + test('rejectUnauthorized default setting email using not secure host/port', async () => { const sendEmailOptions = getSendEmailOptions({ transport: { host: 'example.com', @@ -80,9 +134,6 @@ describe('send_email module', () => { "host": "example.com", "port": 1025, "secure": false, - "tls": Object { - "rejectUnauthorized": false, - }, }, ] `); @@ -161,7 +212,10 @@ describe('send_email module', () => { }); }); -function getSendEmailOptions({ content = {}, routing = {}, transport = {} } = {}) { +function getSendEmailOptions( + { content = {}, routing = {}, transport = {} } = {}, + proxySettings?: ProxySettings +) { return { content: { ...content, @@ -181,5 +235,6 @@ function getSendEmailOptions({ content = {}, routing = {}, transport = {} } = {} user: 'elastic', password: 'changeme', }, + proxySettings, }; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts index 869db34f034ae..a4f32f1880cb5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts @@ -6,10 +6,10 @@ // info on nodemailer: https://nodemailer.com/about/ import nodemailer from 'nodemailer'; - import { default as MarkdownIt } from 'markdown-it'; import { Logger } from '../../../../../../src/core/server'; +import { ProxySettings } from '../../types'; // an email "service" which doesn't actually send, just returns what it would send export const JSON_TRANSPORT_SERVICE = '__json'; @@ -18,6 +18,7 @@ export interface SendEmailOptions { transport: Transport; routing: Routing; content: Content; + proxySettings?: ProxySettings; } // config validation ensures either service is set or host/port are set @@ -44,7 +45,7 @@ export interface Content { // send an email export async function sendEmail(logger: Logger, options: SendEmailOptions): Promise { - const { transport, routing, content } = options; + const { transport, routing, content, proxySettings } = options; const { service, host, port, secure, user, password } = transport; const { from, to, cc, bcc } = routing; const { subject, message } = content; @@ -67,11 +68,16 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom transportConfig.host = host; transportConfig.port = port; transportConfig.secure = !!secure; - if (!transportConfig.secure) { + if (proxySettings && !transportConfig.secure) { transportConfig.tls = { - rejectUnauthorized: false, + // do not fail on invalid certs if value is false + rejectUnauthorized: proxySettings?.rejectUnauthorizedCertificates, }; } + if (proxySettings) { + transportConfig.proxy = proxySettings.proxyUrl; + transportConfig.headers = proxySettings.proxyHeaders; + } } const nodemailerTransport = nodemailer.createTransport(transportConfig); diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts index b76e57419bc56..c0edfc530e738 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts @@ -161,6 +161,7 @@ async function executor( const secrets = execOptions.secrets; const params = execOptions.params; const services = execOptions.services; + const proxySettings = execOptions.proxySettings; const apiUrl = getPagerDutyApiUrl(config); const headers = { @@ -171,7 +172,7 @@ async function executor( let response; try { - response = await postPagerduty({ apiUrl, data, headers, services }); + response = await postPagerduty({ apiUrl, data, headers, services, proxySettings }, logger); } catch (err) { const message = i18n.translate('xpack.actions.builtin.pagerduty.postingErrorMessage', { defaultMessage: 'error posting pagerduty event', diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts index e98bc71559d3f..1e9cb15589702 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Logger } from '../../../../../../src/core/server'; import { createConnector } from '../case/utils'; import { api } from './api'; @@ -11,14 +12,25 @@ import { config } from './config'; import { validate } from './validators'; import { createExternalService } from './service'; import { ResilientSecretConfiguration, ResilientPublicConfiguration } from './schema'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { ActionType } from '../../types'; -export const getActionType = createConnector({ - api, - config, - validate, - createExternalService, - validationSchema: { - config: ResilientPublicConfiguration, - secrets: ResilientSecretConfiguration, - }, -}); +export function getActionType({ + logger, + configurationUtilities, +}: { + logger: Logger; + configurationUtilities: ActionsConfigurationUtilities; +}): ActionType { + return createConnector({ + api, + config, + validate, + createExternalService, + validationSchema: { + config: ResilientPublicConfiguration, + secrets: ResilientSecretConfiguration, + }, + logger, + })({ configurationUtilities }); +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts index 573885698014e..a9271671f68b9 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts @@ -9,6 +9,9 @@ import axios from 'axios'; import { createExternalService, getValueTextContent, formatUpdateRequest } from './service'; import * as utils from '../lib/axios_utils'; import { ExternalService } from '../case/types'; +import { Logger } from '../../../../../../src/core/server'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +const logger = loggingSystemMock.create().get() as jest.Mocked; jest.mock('axios'); jest.mock('../lib/axios_utils', () => { @@ -72,10 +75,13 @@ describe('IBM Resilient service', () => { let service: ExternalService; beforeAll(() => { - service = createExternalService({ - config: { apiUrl: 'https://resilient.elastic.co', orgId: '201' }, - secrets: { apiKeyId: 'keyId', apiKeySecret: 'secret' }, - }); + service = createExternalService( + { + config: { apiUrl: 'https://resilient.elastic.co', orgId: '201' }, + secrets: { apiKeyId: 'keyId', apiKeySecret: 'secret' }, + }, + logger + ); }); afterAll(() => { @@ -138,37 +144,49 @@ describe('IBM Resilient service', () => { describe('createExternalService', () => { test('throws without url', () => { expect(() => - createExternalService({ - config: { apiUrl: null, orgId: '201' }, - secrets: { apiKeyId: 'token', apiKeySecret: 'secret' }, - }) + createExternalService( + { + config: { apiUrl: null, orgId: '201' }, + secrets: { apiKeyId: 'token', apiKeySecret: 'secret' }, + }, + logger + ) ).toThrow(); }); test('throws without orgId', () => { expect(() => - createExternalService({ - config: { apiUrl: 'test.com', orgId: null }, - secrets: { apiKeyId: 'token', apiKeySecret: 'secret' }, - }) + createExternalService( + { + config: { apiUrl: 'test.com', orgId: null }, + secrets: { apiKeyId: 'token', apiKeySecret: 'secret' }, + }, + logger + ) ).toThrow(); }); test('throws without username', () => { expect(() => - createExternalService({ - config: { apiUrl: 'test.com', orgId: '201' }, - secrets: { apiKeyId: '', apiKeySecret: 'secret' }, - }) + createExternalService( + { + config: { apiUrl: 'test.com', orgId: '201' }, + secrets: { apiKeyId: '', apiKeySecret: 'secret' }, + }, + logger + ) ).toThrow(); }); test('throws without password', () => { expect(() => - createExternalService({ - config: { apiUrl: 'test.com', orgId: '201' }, - secrets: { apiKeyId: '', apiKeySecret: undefined }, - }) + createExternalService( + { + config: { apiUrl: 'test.com', orgId: '201' }, + secrets: { apiKeyId: '', apiKeySecret: undefined }, + }, + logger + ) ).toThrow(); }); }); @@ -197,6 +215,7 @@ describe('IBM Resilient service', () => { await service.getIncident('1'); expect(requestMock).toHaveBeenCalledWith({ axios, + logger, url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1', params: { text_content_output_format: 'objects_convert', @@ -256,6 +275,7 @@ describe('IBM Resilient service', () => { expect(requestMock).toHaveBeenCalledWith({ axios, url: 'https://resilient.elastic.co/rest/orgs/201/incidents', + logger, method: 'post', data: { name: 'title', @@ -311,6 +331,7 @@ describe('IBM Resilient service', () => { // The second call to the API is the update call. expect(requestMock.mock.calls[1][0]).toEqual({ axios, + logger, method: 'patch', url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1', data: { @@ -392,7 +413,9 @@ describe('IBM Resilient service', () => { expect(requestMock).toHaveBeenCalledWith({ axios, + logger, method: 'post', + proxySettings: undefined, url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1/comments', data: { text: { diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts index 8d0526ca3b571..b2150081f2c89 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts @@ -6,6 +6,7 @@ import axios from 'axios'; +import { Logger } from '../../../../../../src/core/server'; import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types'; import { ResilientPublicConfigurationType, @@ -19,6 +20,7 @@ import { import * as i18n from './translations'; import { getErrorMessage, request } from '../lib/axios_utils'; +import { ProxySettings } from '../../types'; const BASE_URL = `rest`; const INCIDENT_URL = `incidents`; @@ -57,10 +59,11 @@ export const formatUpdateRequest = ({ }; }; -export const createExternalService = ({ - config, - secrets, -}: ExternalServiceCredentials): ExternalService => { +export const createExternalService = ( + { config, secrets }: ExternalServiceCredentials, + logger: Logger, + proxySettings?: ProxySettings +): ExternalService => { const { apiUrl: url, orgId } = config as ResilientPublicConfigurationType; const { apiKeyId, apiKeySecret } = secrets as ResilientSecretConfigurationType; @@ -88,9 +91,11 @@ export const createExternalService = ({ const res = await request({ axios: axiosInstance, url: `${incidentUrl}/${id}`, + logger, params: { text_content_output_format: 'objects_convert', }, + proxySettings, }); return { ...res.data, description: res.data.description?.content ?? '' }; @@ -107,6 +112,7 @@ export const createExternalService = ({ axios: axiosInstance, url: `${incidentUrl}`, method: 'post', + logger, data: { ...incident, description: { @@ -115,6 +121,7 @@ export const createExternalService = ({ }, discovered_date: Date.now(), }, + proxySettings, }); return { @@ -139,7 +146,9 @@ export const createExternalService = ({ axios: axiosInstance, method: 'patch', url: `${incidentUrl}/${incidentId}`, + logger, data, + proxySettings, }); if (!res.data.success) { @@ -170,7 +179,9 @@ export const createExternalService = ({ axios: axiosInstance, method: 'post', url: getCommentsURL(incidentId), + logger, data: { text: { format: 'text', content: comment.comment } }, + proxySettings, }); return { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index 109008b8fc9fb..3addbe7c54dac 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -76,10 +76,14 @@ async function executor( const { subAction, subActionParams } = params; let data: PushToServiceResponse | null = null; - const externalService = createExternalService({ - config, - secrets, - }); + const externalService = createExternalService( + { + config, + secrets, + }, + logger, + execOptions.proxySettings + ); if (!api[subAction]) { const errorMessage = `[Action][ExternalService] Unsupported subAction type ${subAction}.`; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts index 07d60ec9f7a05..2adcdf561ce17 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts @@ -9,6 +9,9 @@ import axios from 'axios'; import { createExternalService } from './service'; import * as utils from '../lib/axios_utils'; import { ExternalService } from './types'; +import { Logger } from '../../../../../../src/core/server'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +const logger = loggingSystemMock.create().get() as jest.Mocked; jest.mock('axios'); jest.mock('../lib/axios_utils', () => { @@ -28,10 +31,13 @@ describe('ServiceNow service', () => { let service: ExternalService; beforeAll(() => { - service = createExternalService({ - config: { apiUrl: 'https://dev102283.service-now.com' }, - secrets: { username: 'admin', password: 'admin' }, - }); + service = createExternalService( + { + config: { apiUrl: 'https://dev102283.service-now.com' }, + secrets: { username: 'admin', password: 'admin' }, + }, + logger + ); }); beforeEach(() => { @@ -41,28 +47,37 @@ describe('ServiceNow service', () => { describe('createExternalService', () => { test('throws without url', () => { expect(() => - createExternalService({ - config: { apiUrl: null }, - secrets: { username: 'admin', password: 'admin' }, - }) + createExternalService( + { + config: { apiUrl: null }, + secrets: { username: 'admin', password: 'admin' }, + }, + logger + ) ).toThrow(); }); test('throws without username', () => { expect(() => - createExternalService({ - config: { apiUrl: 'test.com' }, - secrets: { username: '', password: 'admin' }, - }) + createExternalService( + { + config: { apiUrl: 'test.com' }, + secrets: { username: '', password: 'admin' }, + }, + logger + ) ).toThrow(); }); test('throws without password', () => { expect(() => - createExternalService({ - config: { apiUrl: 'test.com' }, - secrets: { username: '', password: undefined }, - }) + createExternalService( + { + config: { apiUrl: 'test.com' }, + secrets: { username: '', password: undefined }, + }, + logger + ) ).toThrow(); }); }); @@ -84,6 +99,7 @@ describe('ServiceNow service', () => { await service.getIncident('1'); expect(requestMock).toHaveBeenCalledWith({ axios, + logger, url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1', }); }); @@ -127,6 +143,7 @@ describe('ServiceNow service', () => { expect(requestMock).toHaveBeenCalledWith({ axios, + logger, url: 'https://dev102283.service-now.com/api/now/v2/table/incident', method: 'post', data: { short_description: 'title', description: 'desc' }, @@ -179,6 +196,7 @@ describe('ServiceNow service', () => { expect(patchMock).toHaveBeenCalledWith({ axios, + logger, url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1', data: { short_description: 'title', description: 'desc' }, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts index 2b5204af2eb7d..cf1c26e6462a2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts @@ -9,8 +9,10 @@ import axios from 'axios'; import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from './types'; import * as i18n from './translations'; +import { Logger } from '../../../../../../src/core/server'; import { ServiceNowPublicConfigurationType, ServiceNowSecretConfigurationType } from './types'; import { request, getErrorMessage, addTimeZoneToDate, patch } from '../lib/axios_utils'; +import { ProxySettings } from '../../types'; const API_VERSION = 'v2'; const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`; @@ -18,10 +20,11 @@ const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`; // Based on: https://docs.servicenow.com/bundle/orlando-platform-user-interface/page/use/navigation/reference/r_NavigatingByURLExamples.html const VIEW_INCIDENT_URL = `nav_to.do?uri=incident.do?sys_id=`; -export const createExternalService = ({ - config, - secrets, -}: ExternalServiceCredentials): ExternalService => { +export const createExternalService = ( + { config, secrets }: ExternalServiceCredentials, + logger: Logger, + proxySettings?: ProxySettings +): ExternalService => { const { apiUrl: url } = config as ServiceNowPublicConfigurationType; const { username, password } = secrets as ServiceNowSecretConfigurationType; @@ -43,6 +46,8 @@ export const createExternalService = ({ const res = await request({ axios: axiosInstance, url: `${incidentUrl}/${id}`, + logger, + proxySettings, }); return { ...res.data.result }; @@ -58,6 +63,8 @@ export const createExternalService = ({ const res = await request({ axios: axiosInstance, url: incidentUrl, + logger, + proxySettings, params, }); @@ -71,9 +78,13 @@ export const createExternalService = ({ const createIncident = async ({ incident }: ExternalServiceParams) => { try { + logger.warn(`incident error : ${JSON.stringify(proxySettings)}`); + logger.warn(`incident error : ${url}`); const res = await request({ axios: axiosInstance, url: `${incidentUrl}`, + logger, + proxySettings, method: 'post', data: { ...(incident as Record) }, }); @@ -96,7 +107,9 @@ export const createExternalService = ({ const res = await patch({ axios: axiosInstance, url: `${incidentUrl}/${incidentId}`, + logger, data: { ...(incident as Record) }, + proxySettings, }); return { diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts index 6d4176067c3ba..812657138152c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts @@ -4,25 +4,40 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Logger } from '../../../../../src/core/server'; import { Services, ActionTypeExecutorResult } from '../types'; import { validateParams, validateSecrets } from '../lib'; import { getActionType, SlackActionType, SlackActionTypeExecutorOptions } from './slack'; import { actionsConfigMock } from '../actions_config.mock'; import { actionsMock } from '../mocks'; +import { createActionTypeRegistry } from './index.test'; + +jest.mock('@slack/webhook', () => { + return { + IncomingWebhook: jest.fn().mockImplementation(() => { + return { send: (message: string) => {} }; + }), + }; +}); const ACTION_TYPE_ID = '.slack'; const services: Services = actionsMock.createServices(); let actionType: SlackActionType; +let mockedLogger: jest.Mocked; beforeAll(() => { + const { logger } = createActionTypeRegistry(); actionType = getActionType({ async executor(options) { return { status: 'ok', actionId: options.actionId }; }, configurationUtilities: actionsConfigMock.create(), + logger, }); + mockedLogger = logger; + expect(actionType).toBeTruthy(); }); describe('action registeration', () => { @@ -83,6 +98,7 @@ describe('validateActionTypeSecrets()', () => { test('should validate and pass when the slack webhookUrl is whitelisted', () => { actionType = getActionType({ + logger: mockedLogger, configurationUtilities: { ...actionsConfigMock.create(), ensureWhitelistedUri: (url) => { @@ -98,9 +114,10 @@ describe('validateActionTypeSecrets()', () => { test('config validation returns an error if the specified URL isnt whitelisted', () => { actionType = getActionType({ + logger: mockedLogger, configurationUtilities: { ...actionsConfigMock.create(), - ensureWhitelistedHostname: (url) => { + ensureWhitelistedHostname: () => { throw new Error(`target hostname is not whitelisted`); }, }, @@ -136,6 +153,7 @@ describe('execute()', () => { actionType = getActionType({ executor: mockSlackExecutor, + logger: mockedLogger, configurationUtilities: actionsConfigMock.create(), }); }); @@ -147,6 +165,10 @@ describe('execute()', () => { config: {}, secrets: { webhookUrl: 'http://example.com' }, params: { message: 'this invocation should succeed' }, + proxySettings: { + proxyUrl: 'https://someproxyhost', + rejectUnauthorizedCertificates: false, + }, }); expect(response).toMatchInlineSnapshot(` Object { @@ -170,4 +192,25 @@ describe('execute()', () => { `"slack mockExecutor failure: this invocation should fail"` ); }); + + test('calls the mock executor with success proxy', async () => { + const actionTypeProxy = getActionType({ + logger: mockedLogger, + configurationUtilities: actionsConfigMock.create(), + }); + await actionTypeProxy.executor({ + actionId: 'some-id', + services, + config: {}, + secrets: { webhookUrl: 'http://example.com' }, + params: { message: 'this invocation should succeed' }, + proxySettings: { + proxyUrl: 'https://someproxyhost', + rejectUnauthorizedCertificates: false, + }, + }); + expect(mockedLogger.info).toHaveBeenCalledWith( + 'IncomingWebhook was called with proxyUrl https://someproxyhost' + ); + }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.ts index 209582585256b..293328c809435 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.ts @@ -6,11 +6,14 @@ import { URL } from 'url'; import { curry } from 'lodash'; +import { HttpsProxyAgent } from 'https-proxy-agent'; +import HttpProxyAgent from 'http-proxy-agent'; import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; import { IncomingWebhook, IncomingWebhookResult } from '@slack/webhook'; import { pipe } from 'fp-ts/lib/pipeable'; import { map, getOrElse } from 'fp-ts/lib/Option'; +import { Logger } from '../../../../../src/core/server'; import { getRetryAfterIntervalFromHeaders } from './lib/http_rersponse_retry_header'; import { @@ -20,6 +23,7 @@ import { ExecutorType, } from '../types'; import { ActionsConfigurationUtilities } from '../actions_config'; +import { getProxyAgent } from './lib/get_proxy_agent'; export type SlackActionType = ActionType<{}, ActionTypeSecretsType, ActionParamsType, unknown>; export type SlackActionTypeExecutorOptions = ActionTypeExecutorOptions< @@ -49,9 +53,11 @@ const ParamsSchema = schema.object({ // customizing executor is only used for tests export function getActionType({ + logger, configurationUtilities, - executor = slackExecutor, + executor = curry(slackExecutor)({ logger }), }: { + logger: Logger; configurationUtilities: ActionsConfigurationUtilities; executor?: ExecutorType<{}, ActionTypeSecretsType, ActionParamsType, unknown>; }): SlackActionType { @@ -99,6 +105,7 @@ function valdiateActionTypeConfig( // action executor async function slackExecutor( + { logger }: { logger: Logger }, execOptions: SlackActionTypeExecutorOptions ): Promise> { const actionId = execOptions.actionId; @@ -109,10 +116,22 @@ async function slackExecutor( const { webhookUrl } = secrets; const { message } = params; + let proxyAgent: HttpsProxyAgent | HttpProxyAgent | undefined; + if (execOptions.proxySettings) { + proxyAgent = getProxyAgent(execOptions.proxySettings, logger); + logger.info(`IncomingWebhook was called with proxyUrl ${execOptions.proxySettings.proxyUrl}`); + } + try { - const webhook = new IncomingWebhook(webhookUrl); + // https://slack.dev/node-slack-sdk/webhook + // node-slack-sdk use Axios inside :) + const webhook = new IncomingWebhook(webhookUrl, { + agent: proxyAgent, + }); result = await webhook.send(message); } catch (err) { + logger.error(`error on ${actionId} slack event: ${err.message}`); + if (err.original == null || err.original.response == null) { return serviceErrorResult(actionId, err.message); } @@ -143,6 +162,8 @@ async function slackExecutor( }, } ); + logger.error(`error on ${actionId} slack action: ${errMessage}`); + return errorResult(actionId, errMessage); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts index 26dd8a1a1402a..ea9f30452918c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts @@ -4,10 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('axios', () => ({ - request: jest.fn(), -})); - import { Services } from '../types'; import { validateConfig, validateSecrets, validateParams } from '../lib'; import { actionsConfigMock } from '../actions_config.mock'; @@ -24,7 +20,22 @@ import { WebhookMethods, } from './webhook'; -const axiosRequestMock = axios.request as jest.Mock; +import * as utils from './lib/axios_utils'; + +jest.mock('axios'); +jest.mock('./lib/axios_utils', () => { + const originalUtils = jest.requireActual('./lib/axios_utils'); + return { + ...originalUtils, + request: jest.fn(), + patch: jest.fn(), + }; +}); + +axios.create = jest.fn(() => axios); +const requestMock = utils.request as jest.Mock; + +axios.create = jest.fn(() => axios); const ACTION_TYPE_ID = '.webhook'; @@ -227,7 +238,7 @@ describe('params validation', () => { describe('execute()', () => { beforeAll(() => { - axiosRequestMock.mockReset(); + requestMock.mockReset(); actionType = getActionType({ logger: mockedLogger, configurationUtilities: actionsConfigMock.create(), @@ -235,8 +246,8 @@ describe('execute()', () => { }); beforeEach(() => { - axiosRequestMock.mockReset(); - axiosRequestMock.mockResolvedValue({ + requestMock.mockReset(); + requestMock.mockResolvedValue({ status: 200, statusText: '', data: '', @@ -261,17 +272,42 @@ describe('execute()', () => { params: { body: 'some data' }, }); - expect(axiosRequestMock.mock.calls[0][0]).toMatchInlineSnapshot(` + expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(` Object { "auth": Object { "password": "123", "username": "abc", }, + "axios": undefined, "data": "some data", "headers": Object { "aheader": "a value", }, + "logger": Object { + "context": Array [], + "debug": [MockFunction] { + "calls": Array [ + Array [ + "response from webhook action \\"some-id\\": [HTTP 200] ", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], + }, + "error": [MockFunction], + "fatal": [MockFunction], + "get": [MockFunction], + "info": [MockFunction], + "log": [MockFunction], + "trace": [MockFunction], + "warn": [MockFunction], + }, "method": "post", + "proxySettings": undefined, "url": "https://abc.def/my-webhook", } `); @@ -294,13 +330,38 @@ describe('execute()', () => { params: { body: 'some data' }, }); - expect(axiosRequestMock.mock.calls[0][0]).toMatchInlineSnapshot(` + expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(` Object { + "axios": undefined, "data": "some data", "headers": Object { "aheader": "a value", }, + "logger": Object { + "context": Array [], + "debug": [MockFunction] { + "calls": Array [ + Array [ + "response from webhook action \\"some-id\\": [HTTP 200] ", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], + }, + "error": [MockFunction], + "fatal": [MockFunction], + "get": [MockFunction], + "info": [MockFunction], + "log": [MockFunction], + "trace": [MockFunction], + "warn": [MockFunction], + }, "method": "post", + "proxySettings": undefined, "url": "https://abc.def/my-webhook", } `); diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts index be75742fa882e..d9a005565498d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts @@ -15,6 +15,7 @@ import { isOk, promiseResult, Result } from './lib/result_type'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; import { ActionsConfigurationUtilities } from '../actions_config'; import { Logger } from '../../../../../src/core/server'; +import { request } from './lib/axios_utils'; // config definition export enum WebhookMethods { @@ -136,13 +137,18 @@ export async function executor( ? { auth: { username: secrets.user, password: secrets.password } } : {}; + const axiosInstance = axios.create(); + const result: Result = await promiseResult( - axios.request({ + request({ + axios: axiosInstance, method, url, + logger, ...basicAuth, headers, data, + proxySettings: execOptions.proxySettings, }) ); @@ -159,7 +165,7 @@ export async function executor( if (error.response) { const { status, statusText, headers: responseHeaders } = error.response; const message = `[${status}] ${statusText}`; - logger.warn(`error on ${actionId} webhook event: ${message}`); + logger.error(`error on ${actionId} webhook event: ${message}`); // The request was made and the server responded with a status code // that falls out of the range of 2xx // special handling for 5xx @@ -178,7 +184,7 @@ export async function executor( return errorResultInvalid(actionId, message); } - logger.warn(`error on ${actionId} webhook action: unexpected error`); + logger.error(`error on ${actionId} webhook action: unexpected error`); return errorResultUnexpectedError(actionId); } } diff --git a/x-pack/plugins/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts index e86f2d7832828..795fbbf84145b 100644 --- a/x-pack/plugins/actions/server/config.test.ts +++ b/x-pack/plugins/actions/server/config.test.ts @@ -15,6 +15,7 @@ describe('config validation', () => { "*", ], "preconfigured": Object {}, + "rejectUnauthorizedCertificates": true, "whitelistedHosts": Array [ "*", ], @@ -33,6 +34,7 @@ describe('config validation', () => { }, }, }, + rejectUnauthorizedCertificates: false, }; expect(configSchema.validate(config)).toMatchInlineSnapshot(` Object { @@ -50,6 +52,7 @@ describe('config validation', () => { "secrets": Object {}, }, }, + "rejectUnauthorizedCertificates": false, "whitelistedHosts": Array [ "*", ], diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts index b2f3fa2680a9c..ba80915ebe243 100644 --- a/x-pack/plugins/actions/server/config.ts +++ b/x-pack/plugins/actions/server/config.ts @@ -32,6 +32,9 @@ export const configSchema = schema.object({ defaultValue: {}, validate: validatePreconfigured, }), + proxyUrl: schema.maybe(schema.string()), + proxyHeaders: schema.maybe(schema.recordOf(schema.string(), schema.string())), + rejectUnauthorizedCertificates: schema.boolean({ defaultValue: true }), }); export type ActionsConfig = TypeOf; diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index bce06c829b1bc..97c08124f5546 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -12,6 +12,7 @@ import { GetServicesFunction, RawAction, PreConfiguredAction, + ProxySettings, } from '../types'; import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; import { SpacesServiceSetup } from '../../../spaces/server'; @@ -28,6 +29,7 @@ export interface ActionExecutorContext { actionTypeRegistry: ActionTypeRegistryContract; eventLogger: IEventLogger; preconfiguredActions: PreConfiguredAction[]; + proxySettings?: ProxySettings; } export interface ExecuteOptions { @@ -78,6 +80,7 @@ export class ActionExecutor { eventLogger, preconfiguredActions, getActionsClientWithRequest, + proxySettings, } = this.actionExecutorContext!; const services = getServices(request); @@ -133,6 +136,7 @@ export class ActionExecutor { params: validatedParams, config: validatedConfig, secrets: validatedSecrets, + proxySettings, }); } catch (err) { rawResult = { diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index ca93e88d01203..341a17889923f 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -34,6 +34,7 @@ describe('Actions Plugin', () => { enabledActionTypes: ['*'], whitelistedHosts: ['*'], preconfigured: {}, + rejectUnauthorizedCertificates: true, }); plugin = new ActionsPlugin(context); coreSetup = coreMock.createSetup(); @@ -194,6 +195,7 @@ describe('Actions Plugin', () => { secrets: {}, }, }, + rejectUnauthorizedCertificates: true, }); plugin = new ActionsPlugin(context); coreSetup = coreMock.createSetup(); @@ -217,7 +219,7 @@ describe('Actions Plugin', () => { // coreMock.createSetup doesn't support Plugin generics // eslint-disable-next-line @typescript-eslint/no-explicit-any await plugin.setup(coreSetup as any, pluginsSetup); - const pluginStart = plugin.start(coreStart, pluginsStart); + const pluginStart = await plugin.start(coreStart, pluginsStart); expect(pluginStart.isActionExecutable('preconfiguredServerLog', '.server-log')).toBe(true); }); @@ -232,7 +234,7 @@ describe('Actions Plugin', () => { usingEphemeralEncryptionKey: false, }, }); - const pluginStart = plugin.start(coreStart, pluginsStart); + const pluginStart = await plugin.start(coreStart, pluginsStart); await pluginStart.getActionsClientWithRequest(httpServerMock.createKibanaRequest()); }); @@ -241,7 +243,7 @@ describe('Actions Plugin', () => { // coreMock.createSetup doesn't support Plugin generics // eslint-disable-next-line @typescript-eslint/no-explicit-any await plugin.setup(coreSetup as any, pluginsSetup); - const pluginStart = plugin.start(coreStart, pluginsStart); + const pluginStart = await plugin.start(coreStart, pluginsStart); expect(pluginsSetup.encryptedSavedObjects.usingEphemeralEncryptionKey).toEqual(true); await expect( diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index ee50ee81d507c..413e6663105b8 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -116,6 +116,7 @@ export class ActionsPlugin implements Plugin, Plugi private readonly config: Promise; private readonly logger: Logger; + private actionsConfig?: ActionsConfig; private serverBasePath?: string; private taskRunnerFactory?: TaskRunnerFactory; private actionTypeRegistry?: ActionTypeRegistry; @@ -173,12 +174,12 @@ export class ActionsPlugin implements Plugin, Plugi // get executions count const taskRunnerFactory = new TaskRunnerFactory(actionExecutor); - const actionsConfig = (await this.config) as ActionsConfig; - const actionsConfigUtils = getActionsConfigurationUtilities(actionsConfig); + this.actionsConfig = (await this.config) as ActionsConfig; + const actionsConfigUtils = getActionsConfigurationUtilities(this.actionsConfig); - for (const preconfiguredId of Object.keys(actionsConfig.preconfigured)) { + for (const preconfiguredId of Object.keys(this.actionsConfig.preconfigured)) { this.preconfiguredActions.push({ - ...actionsConfig.preconfigured[preconfiguredId], + ...this.actionsConfig.preconfigured[preconfiguredId], id: preconfiguredId, isPreconfigured: true, }); @@ -317,6 +318,14 @@ export class ActionsPlugin implements Plugin, Plugi encryptedSavedObjectsClient, actionTypeRegistry: actionTypeRegistry!, preconfiguredActions, + proxySettings: + this.actionsConfig && this.actionsConfig.proxyUrl + ? { + proxyUrl: this.actionsConfig.proxyUrl, + proxyHeaders: this.actionsConfig.proxyHeaders, + rejectUnauthorizedCertificates: this.actionsConfig.rejectUnauthorizedCertificates, + } + : undefined, }); taskRunnerFactory!.initialize({ diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index ecec45ade0460..bf7bd709a4a88 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -58,6 +58,7 @@ export interface ActionTypeExecutorOptions { config: Config; secrets: Secrets; params: Params; + proxySettings?: ProxySettings; } export interface ActionResult { @@ -140,3 +141,9 @@ export interface ActionTaskExecutorParams { spaceId: string; actionTaskParamsId: string; } + +export interface ProxySettings { + proxyUrl: string; + proxyHeaders?: Record; + rejectUnauthorizedCertificates: boolean; +} diff --git a/x-pack/test/alerting_api_integration/basic/config.ts b/x-pack/test/alerting_api_integration/basic/config.ts index f9c248ec3d56f..f58b7753b74f7 100644 --- a/x-pack/test/alerting_api_integration/basic/config.ts +++ b/x-pack/test/alerting_api_integration/basic/config.ts @@ -11,4 +11,5 @@ export default createTestConfig('basic', { disabledPlugins: [], license: 'basic', ssl: true, + enableActionsProxy: false, }); diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 4947cdbf55484..34e23a2dba0b2 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -5,6 +5,7 @@ */ import path from 'path'; +import getPort from 'get-port'; import fs from 'fs'; import { CA_CERT_PATH } from '@kbn/dev-utils'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; @@ -15,6 +16,7 @@ interface CreateTestConfigOptions { license: string; disabledPlugins?: string[]; ssl?: boolean; + enableActionsProxy: boolean; } // test.not-enabled is specifically not enabled @@ -56,6 +58,10 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) fs.statSync(path.resolve(__dirname, 'fixtures', 'plugins', file)).isDirectory() ); + const actionsProxyUrl = options.enableActionsProxy + ? [`--xpack.actions.proxyUrl=http://localhost:${await getPort()}`] + : []; + return { testFiles: [require.resolve(`../${name}/tests/`)], servers, @@ -85,6 +91,9 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ])}`, '--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, + ...actionsProxyUrl, + '--xpack.actions.rejectUnauthorizedCertificates=false', + '--xpack.eventLog.logEntries=true', `--xpack.actions.preconfigured=${JSON.stringify({ 'my-slack1': { diff --git a/x-pack/test/alerting_api_integration/common/lib/get_proxy_server.ts b/x-pack/test/alerting_api_integration/common/lib/get_proxy_server.ts new file mode 100644 index 0000000000000..4540556e73c5f --- /dev/null +++ b/x-pack/test/alerting_api_integration/common/lib/get_proxy_server.ts @@ -0,0 +1,30 @@ +/* + * 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 httpProxy from 'http-proxy'; + +export const getHttpProxyServer = ( + targetUrl: string, + onProxyResHandler: (proxyRes?: unknown, req?: unknown, res?: unknown) => void +): httpProxy => { + const proxyServer = httpProxy.createProxyServer({ + target: targetUrl, + secure: false, + selfHandleResponse: false, + }); + proxyServer.on('proxyRes', (proxyRes: unknown, req: unknown, res: unknown) => { + onProxyResHandler(proxyRes, req, res); + }); + return proxyServer; +}; + +export const getProxyUrl = (kbnTestServerConfig: any) => { + const proxyUrl = kbnTestServerConfig + .find((val: string) => val.startsWith('--xpack.actions.proxyUrl=')) + .replace('--xpack.actions.proxyUrl=', ''); + + return new URL(proxyUrl); +}; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/config.ts b/x-pack/test/alerting_api_integration/security_and_spaces/config.ts index 081b901c47fc3..97f53ae2c3664 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/config.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/config.ts @@ -11,4 +11,5 @@ export default createTestConfig('security_and_spaces', { disabledPlugins: [], license: 'trial', ssl: true, + enableActionsProxy: true, }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts index 24931f11d4999..a0ba5331105bc 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -6,6 +6,7 @@ import expect from '@kbn/expect'; +import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -35,6 +36,7 @@ const mapping = [ export default function jiraTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); + const config = getService('config'); const mockJira = { config: { @@ -73,12 +75,19 @@ export default function jiraTest({ getService }: FtrProviderContext) { }; let jiraSimulatorURL: string = ''; + let proxyServer: any; + let proxyHaveBeenCalled = false; describe('Jira', () => { before(() => { jiraSimulatorURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.JIRA) ); + proxyServer = getHttpProxyServer(kibanaServer.resolveUrl('/'), () => { + proxyHaveBeenCalled = true; + }); + const proxyUrl = getProxyUrl(config.get('kbnTestServer.serverArgs')); + proxyServer.listen(Number(proxyUrl.port)); }); describe('Jira - Action Creation', () => { @@ -529,6 +538,8 @@ export default function jiraTest({ getService }: FtrProviderContext) { }) .expect(200); + expect(proxyHaveBeenCalled).to.equal(true); + expect(body).to.eql({ status: 'ok', actionId: simulatedActionId, @@ -542,5 +553,9 @@ export default function jiraTest({ getService }: FtrProviderContext) { }); }); }); + + after(() => { + proxyServer.close(); + }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts index f4fcbb65ab5a3..c697cf69bb4d5 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts @@ -6,6 +6,7 @@ import expect from '@kbn/expect'; +import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -17,16 +18,25 @@ import { export default function pagerdutyTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); + const config = getService('config'); describe('pagerduty action', () => { let simulatedActionId = ''; let pagerdutySimulatorURL: string = ''; + let proxyServer: any; + let proxyHaveBeenCalled = false; // need to wait for kibanaServer to settle ... before(() => { pagerdutySimulatorURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.PAGERDUTY) ); + + proxyServer = getHttpProxyServer(kibanaServer.resolveUrl('/'), () => { + proxyHaveBeenCalled = true; + }); + const proxyUrl = getProxyUrl(config.get('kbnTestServer.serverArgs')); + proxyServer.listen(Number(proxyUrl.port)); }); it('should return successfully when passed valid create parameters', async () => { @@ -144,6 +154,8 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { }, }) .expect(200); + expect(proxyHaveBeenCalled).to.equal(true); + expect(result).to.eql({ status: 'ok', actionId: simulatedActionId, @@ -202,5 +214,9 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { expect(result.message).to.match(/error posting pagerduty event: http status 502/); expect(result.retry).to.equal(true); }); + + after(() => { + proxyServer.close(); + }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts index 94feabb556a51..5085c87550d01 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts @@ -6,6 +6,7 @@ import expect from '@kbn/expect'; +import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -35,6 +36,7 @@ const mapping = [ export default function resilientTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); + const config = getService('config'); const mockResilient = { config: { @@ -73,12 +75,19 @@ export default function resilientTest({ getService }: FtrProviderContext) { }; let resilientSimulatorURL: string = ''; + let proxyServer: any; + let proxyHaveBeenCalled = false; describe('IBM Resilient', () => { before(() => { resilientSimulatorURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.RESILIENT) ); + proxyServer = getHttpProxyServer(kibanaServer.resolveUrl('/'), () => { + proxyHaveBeenCalled = true; + }); + const proxyUrl = getProxyUrl(config.get('kbnTestServer.serverArgs')); + proxyServer.listen(Number(proxyUrl.port)); }); describe('IBM Resilient - Action Creation', () => { @@ -529,6 +538,8 @@ export default function resilientTest({ getService }: FtrProviderContext) { }) .expect(200); + expect(proxyHaveBeenCalled).to.equal(true); + expect(body).to.eql({ status: 'ok', actionId: simulatedActionId, @@ -542,5 +553,9 @@ export default function resilientTest({ getService }: FtrProviderContext) { }); }); }); + + after(() => { + proxyServer.close(); + }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts index d3b72d01216d0..70b6a8fe512e1 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts @@ -6,6 +6,7 @@ import expect from '@kbn/expect'; +import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -35,6 +36,7 @@ const mapping = [ export default function servicenowTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); + const config = getService('config'); const mockServiceNow = { config: { @@ -72,12 +74,20 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }; let servicenowSimulatorURL: string = ''; + let proxyServer: any; + let proxyHaveBeenCalled = false; describe('ServiceNow', () => { before(() => { servicenowSimulatorURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) ); + + proxyServer = getHttpProxyServer(kibanaServer.resolveUrl('/'), () => { + proxyHaveBeenCalled = true; + }); + const proxyUrl = getProxyUrl(config.get('kbnTestServer.serverArgs')); + proxyServer.listen(Number(proxyUrl.port)); }); describe('ServiceNow - Action Creation', () => { @@ -448,6 +458,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }, }) .expect(200); + expect(proxyHaveBeenCalled).to.equal(true); expect(result).to.eql({ status: 'ok', @@ -462,5 +473,9 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }); }); }); + + after(() => { + proxyServer.close(); + }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts index c68bcaa0ad4e8..45f9ba369dc23 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import http from 'http'; import getPort from 'get-port'; +import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getSlackServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; @@ -14,18 +15,27 @@ import { getSlackServer } from '../../../../common/fixtures/plugins/actions_simu // eslint-disable-next-line import/no-default-export export default function slackTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const config = getService('config'); describe('slack action', () => { let simulatedActionId = ''; let slackSimulatorURL: string = ''; let slackServer: http.Server; + let proxyServer: any; + let proxyHaveBeenCalled = false; // need to wait for kibanaServer to settle ... before(async () => { slackServer = await getSlackServer(); const availablePort = await getPort({ port: 9000 }); slackServer.listen(availablePort); slackSimulatorURL = `http://localhost:${availablePort}`; + + proxyServer = getHttpProxyServer(slackSimulatorURL, () => { + proxyHaveBeenCalled = true; + }); + const proxyUrl = getProxyUrl(config.get('kbnTestServer.serverArgs')); + proxyServer.listen(Number(proxyUrl.port)); }); it('should return 200 when creating a slack action successfully', async () => { @@ -155,6 +165,7 @@ export default function slackTest({ getService }: FtrProviderContext) { }) .expect(200); expect(result.status).to.eql('ok'); + expect(proxyHaveBeenCalled).to.equal(true); }); it('should handle an empty message error', async () => { @@ -222,6 +233,7 @@ export default function slackTest({ getService }: FtrProviderContext) { after(() => { slackServer.close(); + proxyServer.close(); }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts index 8f17ab54184b5..896026611043f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts @@ -8,6 +8,7 @@ import http from 'http'; import getPort from 'get-port'; import expect from '@kbn/expect'; import { URL, format as formatUrl } from 'url'; +import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, @@ -31,6 +32,7 @@ function parsePort(url: Record): Record { webhookServer = await getWebhookServer(); @@ -76,6 +80,12 @@ export default function webhookTest({ getService }: FtrProviderContext) { webhookServer.listen(availablePort); webhookSimulatorURL = `http://localhost:${availablePort}`; + proxyServer = getHttpProxyServer(webhookSimulatorURL, () => { + proxyHaveBeenCalled = true; + }); + const proxyUrl = getProxyUrl(configService.get('kbnTestServer.serverArgs')); + proxyServer.listen(Number(proxyUrl.port)); + kibanaURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.WEBHOOK) ); @@ -140,6 +150,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { .expect(200); expect(result.status).to.eql('ok'); + expect(proxyHaveBeenCalled).to.equal(true); }); it('should support the POST method against webhook target', async () => { @@ -218,7 +229,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { .expect(200); expect(result.status).to.eql('error'); - expect(result.message).to.match(/error calling webhook, unexpected error/); + expect(result.message).to.match(/error calling webhook, retry later/); }); it('should handle failing webhook targets', async () => { @@ -240,6 +251,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { after(() => { webhookServer.close(); + proxyServer.close(); }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/config.ts b/x-pack/test/alerting_api_integration/spaces_only/config.ts index c79c26ef68752..f9860b642f13a 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/config.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/config.ts @@ -7,4 +7,8 @@ import { createTestConfig } from '../common/config'; // eslint-disable-next-line import/no-default-export -export default createTestConfig('spaces_only', { disabledPlugins: ['security'], license: 'trial' }); +export default createTestConfig('spaces_only', { + disabledPlugins: ['security'], + license: 'trial', + enableActionsProxy: false, +}); diff --git a/yarn.lock b/yarn.lock index 42c4b800e6b0c..b730aac686c8a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3919,6 +3919,20 @@ resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz#9140779736aa2655635ee756e2467d787cfe8a2a" integrity sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A== +"@types/http-proxy-agent@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/http-proxy-agent/-/http-proxy-agent-2.0.2.tgz#942c1f35c7e1f0edd1b6ffae5d0f9051cfb32be1" + integrity sha512-2S6IuBRhqUnH1/AUx9k8KWtY3Esg4eqri946MnxTG5HwehF1S5mqLln8fcyMiuQkY72p2gH3W+rIPqp5li0LyQ== + dependencies: + "@types/node" "*" + +"@types/http-proxy@^1.17.4": + version "1.17.4" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.4.tgz#e7c92e3dbe3e13aa799440ff42e6d3a17a9d045b" + integrity sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q== + dependencies: + "@types/node" "*" + "@types/inert@^5.1.2": version "5.1.2" resolved "https://registry.yarnpkg.com/@types/inert/-/inert-5.1.2.tgz#2bb8bef3b2462f904c960654c9edfa39285a85c6"