Actions add proxy support (#74289)

* Added proxy support for action types

* Fixed tests

* added rejectUnauthorizedCertificates config setting

* removed slack not used code

* Fixed Slack proxy

* fixed typecheck errors

* Cleanup code

* Fixed slack

* Added unit tests

* added proxy server for test

* Fixed build

* Added functional tests

* fixed due to comments

* Fixed tests and some changes due to comments

* Fixed functional tests

* fixed circular deps

* Added proxy unit test to action type
This commit is contained in:
Yuliia Naumenko 2020-08-14 14:20:12 -07:00 committed by GitHub
parent 64b8b88c64
commit 52bd6d98ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 830 additions and 198 deletions

View file

@ -44,9 +44,9 @@
"@storybook/addon-storyshots": "^5.3.19", "@storybook/addon-storyshots": "^5.3.19",
"@storybook/react": "^5.3.19", "@storybook/react": "^5.3.19",
"@storybook/theming": "^5.3.19", "@storybook/theming": "^5.3.19",
"@testing-library/jest-dom": "^5.8.0",
"@testing-library/react": "^9.3.2", "@testing-library/react": "^9.3.2",
"@testing-library/react-hooks": "^3.2.1", "@testing-library/react-hooks": "^3.2.1",
"@testing-library/jest-dom": "^5.8.0",
"@types/angular": "^1.6.56", "@types/angular": "^1.6.56",
"@types/archiver": "^3.1.0", "@types/archiver": "^3.1.0",
"@types/base64-js": "^1.2.5", "@types/base64-js": "^1.2.5",
@ -72,8 +72,9 @@
"@types/gulp": "^4.0.6", "@types/gulp": "^4.0.6",
"@types/hapi__wreck": "^15.0.1", "@types/hapi__wreck": "^15.0.1",
"@types/he": "^1.1.1", "@types/he": "^1.1.1",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/history": "^4.7.3", "@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": "^25.2.3",
"@types/jest-specific-snapshot": "^0.5.4", "@types/jest-specific-snapshot": "^0.5.4",
"@types/joi": "^13.4.2", "@types/joi": "^13.4.2",
@ -94,6 +95,7 @@
"@types/object-hash": "^1.3.0", "@types/object-hash": "^1.3.0",
"@types/papaparse": "^5.0.3", "@types/papaparse": "^5.0.3",
"@types/pngjs": "^3.3.2", "@types/pngjs": "^3.3.2",
"@types/pretty-ms": "^5.0.0",
"@types/prop-types": "^15.5.3", "@types/prop-types": "^15.5.3",
"@types/proper-lockfile": "^3.0.1", "@types/proper-lockfile": "^3.0.1",
"@types/puppeteer": "^1.20.1", "@types/puppeteer": "^1.20.1",
@ -109,6 +111,7 @@
"@types/redux-actions": "^2.6.1", "@types/redux-actions": "^2.6.1",
"@types/set-value": "^2.0.0", "@types/set-value": "^2.0.0",
"@types/sinon": "^7.0.13", "@types/sinon": "^7.0.13",
"@types/stats-lite": "^2.2.0",
"@types/styled-components": "^5.1.0", "@types/styled-components": "^5.1.0",
"@types/supertest": "^2.0.5", "@types/supertest": "^2.0.5",
"@types/tar-fs": "^1.16.1", "@types/tar-fs": "^1.16.1",
@ -116,11 +119,9 @@
"@types/tinycolor2": "^1.4.1", "@types/tinycolor2": "^1.4.1",
"@types/use-resize-observer": "^6.0.0", "@types/use-resize-observer": "^6.0.0",
"@types/uuid": "^3.4.4", "@types/uuid": "^3.4.4",
"@types/webpack-env": "^1.15.2",
"@types/xml-crypto": "^1.4.0", "@types/xml-crypto": "^1.4.0",
"@types/xml2js": "^0.4.5", "@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", "@welldone-software/why-did-you-render": "^4.0.0",
"abab": "^1.0.4", "abab": "^1.0.4",
"autoprefixer": "^9.7.4", "autoprefixer": "^9.7.4",
@ -227,6 +228,7 @@
"@turf/circle": "6.0.1", "@turf/circle": "6.0.1",
"@turf/distance": "6.0.1", "@turf/distance": "6.0.1",
"@turf/helpers": "6.0.1", "@turf/helpers": "6.0.1",
"@types/http-proxy-agent": "^2.0.2",
"angular": "^1.8.0", "angular": "^1.8.0",
"angular-resource": "1.8.0", "angular-resource": "1.8.0",
"angular-sanitize": "1.8.0", "angular-sanitize": "1.8.0",

View file

@ -9,6 +9,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { TypeOf } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema';
import { Logger } from '../../../../../../src/core/server';
import { import {
ExternalIncidentServiceConfigurationSchema, ExternalIncidentServiceConfigurationSchema,
@ -122,7 +123,12 @@ export interface ExternalServiceApi {
export interface CreateExternalServiceBasicArgs { export interface CreateExternalServiceBasicArgs {
api: ExternalServiceApi; api: ExternalServiceApi;
createExternalService: (credentials: ExternalServiceCredentials) => ExternalService; createExternalService: (
credentials: ExternalServiceCredentials,
logger: Logger,
proxySettings?: any
) => ExternalService;
logger: Logger;
} }
export interface CreateExternalServiceArgs extends CreateExternalServiceBasicArgs { export interface CreateExternalServiceArgs extends CreateExternalServiceBasicArgs {

View file

@ -67,6 +67,7 @@ export const mapParams = (
export const createConnectorExecutor = ({ export const createConnectorExecutor = ({
api, api,
createExternalService, createExternalService,
logger,
}: CreateExternalServiceBasicArgs) => async ( }: CreateExternalServiceBasicArgs) => async (
execOptions: ActionTypeExecutorOptions< execOptions: ActionTypeExecutorOptions<
ExternalIncidentServiceConfiguration, ExternalIncidentServiceConfiguration,
@ -83,10 +84,14 @@ export const createConnectorExecutor = ({
actionId, actionId,
}; };
const externalService = createExternalService({ const externalService = createExternalService(
config, {
secrets, config,
}); secrets,
},
logger,
execOptions.proxySettings
);
if (!api[subAction]) { if (!api[subAction]) {
throw new Error('[Action][ExternalService] Unsupported subAction type.'); throw new Error('[Action][ExternalService] Unsupported subAction type.');
@ -122,10 +127,11 @@ export const createConnector = ({
validate, validate,
createExternalService, createExternalService,
validationSchema, validationSchema,
logger,
}: CreateExternalServiceArgs) => { }: CreateExternalServiceArgs) => {
return ({ return ({
configurationUtilities, configurationUtilities,
executor = createConnectorExecutor({ api, createExternalService }), executor = createConnectorExecutor({ api, createExternalService, logger }),
}: CreateActionTypeArgs): ActionType => ({ }: CreateActionTypeArgs): ActionType => ({
...config, ...config,
validate: { validate: {

View file

@ -269,6 +269,7 @@ describe('execute()', () => {
"message": "a message to you", "message": "a message to you",
"subject": "the subject", "subject": "the subject",
}, },
"proxySettings": undefined,
"routing": Object { "routing": Object {
"bcc": Array [ "bcc": Array [
"jimmy@example.com", "jimmy@example.com",
@ -326,6 +327,7 @@ describe('execute()', () => {
"message": "a message to you", "message": "a message to you",
"subject": "the subject", "subject": "the subject",
}, },
"proxySettings": undefined,
"routing": Object { "routing": Object {
"bcc": Array [ "bcc": Array [
"jimmy@example.com", "jimmy@example.com",

View file

@ -184,6 +184,7 @@ async function executor(
subject: params.subject, subject: params.subject,
message: params.message, message: params.message,
}, },
proxySettings: execOptions.proxySettings,
}; };
let result; let result;

View file

@ -31,9 +31,9 @@ export function registerBuiltInActionTypes({
actionTypeRegistry.register(getIndexActionType({ logger })); actionTypeRegistry.register(getIndexActionType({ logger }));
actionTypeRegistry.register(getPagerDutyActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getPagerDutyActionType({ logger, configurationUtilities }));
actionTypeRegistry.register(getServerLogActionType({ logger })); actionTypeRegistry.register(getServerLogActionType({ logger }));
actionTypeRegistry.register(getSlackActionType({ configurationUtilities })); actionTypeRegistry.register(getSlackActionType({ logger, configurationUtilities }));
actionTypeRegistry.register(getWebhookActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getWebhookActionType({ logger, configurationUtilities }));
actionTypeRegistry.register(getServiceNowActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getServiceNowActionType({ logger, configurationUtilities }));
actionTypeRegistry.register(getJiraActionType({ configurationUtilities })); actionTypeRegistry.register(getJiraActionType({ logger, configurationUtilities }));
actionTypeRegistry.register(getResilientActionType({ configurationUtilities })); actionTypeRegistry.register(getResilientActionType({ logger, configurationUtilities }));
} }

View file

@ -4,21 +4,33 @@
* you may not use this file except in compliance with the Elastic License. * 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 { createConnector } from '../case/utils';
import { ActionType } from '../../types';
import { api } from './api'; import { api } from './api';
import { config } from './config'; import { config } from './config';
import { validate } from './validators'; import { validate } from './validators';
import { createExternalService } from './service'; import { createExternalService } from './service';
import { JiraSecretConfiguration, JiraPublicConfiguration } from './schema'; import { JiraSecretConfiguration, JiraPublicConfiguration } from './schema';
import { ActionsConfigurationUtilities } from '../../actions_config';
export const getActionType = createConnector({ export function getActionType({
api, logger,
config, configurationUtilities,
validate, }: {
createExternalService, logger: Logger;
validationSchema: { configurationUtilities: ActionsConfigurationUtilities;
config: JiraPublicConfiguration, }): ActionType {
secrets: JiraSecretConfiguration, return createConnector({
}, api,
}); config,
validate,
createExternalService,
validationSchema: {
config: JiraPublicConfiguration,
secrets: JiraSecretConfiguration,
},
logger,
})({ configurationUtilities });
}

View file

@ -9,6 +9,9 @@ import axios from 'axios';
import { createExternalService } from './service'; import { createExternalService } from './service';
import * as utils from '../lib/axios_utils'; import * as utils from '../lib/axios_utils';
import { ExternalService } from '../case/types'; 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<Logger>;
jest.mock('axios'); jest.mock('axios');
jest.mock('../lib/axios_utils', () => { jest.mock('../lib/axios_utils', () => {
@ -26,10 +29,13 @@ describe('Jira service', () => {
let service: ExternalService; let service: ExternalService;
beforeAll(() => { beforeAll(() => {
service = createExternalService({ service = createExternalService(
config: { apiUrl: 'https://siem-kibana.atlassian.net', projectKey: 'CK' }, {
secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, config: { apiUrl: 'https://siem-kibana.atlassian.net', projectKey: 'CK' },
}); secrets: { apiToken: 'token', email: 'elastic@elastic.com' },
},
logger
);
}); });
beforeEach(() => { beforeEach(() => {
@ -39,37 +45,49 @@ describe('Jira service', () => {
describe('createExternalService', () => { describe('createExternalService', () => {
test('throws without url', () => { test('throws without url', () => {
expect(() => expect(() =>
createExternalService({ createExternalService(
config: { apiUrl: null, projectKey: 'CK' }, {
secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, config: { apiUrl: null, projectKey: 'CK' },
}) secrets: { apiToken: 'token', email: 'elastic@elastic.com' },
},
logger
)
).toThrow(); ).toThrow();
}); });
test('throws without projectKey', () => { test('throws without projectKey', () => {
expect(() => expect(() =>
createExternalService({ createExternalService(
config: { apiUrl: 'test.com', projectKey: null }, {
secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, config: { apiUrl: 'test.com', projectKey: null },
}) secrets: { apiToken: 'token', email: 'elastic@elastic.com' },
},
logger
)
).toThrow(); ).toThrow();
}); });
test('throws without username', () => { test('throws without username', () => {
expect(() => expect(() =>
createExternalService({ createExternalService(
config: { apiUrl: 'test.com' }, {
secrets: { apiToken: '', email: 'elastic@elastic.com' }, config: { apiUrl: 'test.com' },
}) secrets: { apiToken: '', email: 'elastic@elastic.com' },
},
logger
)
).toThrow(); ).toThrow();
}); });
test('throws without password', () => { test('throws without password', () => {
expect(() => expect(() =>
createExternalService({ createExternalService(
config: { apiUrl: 'test.com' }, {
secrets: { apiToken: '', email: undefined }, config: { apiUrl: 'test.com' },
}) secrets: { apiToken: '', email: undefined },
},
logger
)
).toThrow(); ).toThrow();
}); });
}); });
@ -92,6 +110,7 @@ describe('Jira service', () => {
expect(requestMock).toHaveBeenCalledWith({ expect(requestMock).toHaveBeenCalledWith({
axios, axios,
url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1', url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1',
logger,
}); });
}); });
@ -146,6 +165,7 @@ describe('Jira service', () => {
expect(requestMock).toHaveBeenCalledWith({ expect(requestMock).toHaveBeenCalledWith({
axios, axios,
url: 'https://siem-kibana.atlassian.net/rest/api/2/issue', url: 'https://siem-kibana.atlassian.net/rest/api/2/issue',
logger,
method: 'post', method: 'post',
data: { data: {
fields: { fields: {
@ -210,6 +230,7 @@ describe('Jira service', () => {
expect(requestMock).toHaveBeenCalledWith({ expect(requestMock).toHaveBeenCalledWith({
axios, axios,
logger,
method: 'put', method: 'put',
url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1', url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1',
data: { fields: { summary: 'title', description: 'desc' } }, data: { fields: { summary: 'title', description: 'desc' } },
@ -272,6 +293,7 @@ describe('Jira service', () => {
expect(requestMock).toHaveBeenCalledWith({ expect(requestMock).toHaveBeenCalledWith({
axios, axios,
logger,
method: 'post', method: 'post',
url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1/comment', url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1/comment',
data: { body: 'comment' }, data: { body: 'comment' },

View file

@ -7,6 +7,7 @@
import axios from 'axios'; import axios from 'axios';
import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types'; import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types';
import { Logger } from '../../../../../../src/core/server';
import { import {
JiraPublicConfigurationType, JiraPublicConfigurationType,
JiraSecretConfigurationType, JiraSecretConfigurationType,
@ -17,6 +18,7 @@ import {
import * as i18n from './translations'; import * as i18n from './translations';
import { request, getErrorMessage } from '../lib/axios_utils'; import { request, getErrorMessage } from '../lib/axios_utils';
import { ProxySettings } from '../../types';
const VERSION = '2'; const VERSION = '2';
const BASE_URL = `rest/api/${VERSION}`; const BASE_URL = `rest/api/${VERSION}`;
@ -25,10 +27,11 @@ const COMMENT_URL = `comment`;
const VIEW_INCIDENT_URL = `browse`; const VIEW_INCIDENT_URL = `browse`;
export const createExternalService = ({ export const createExternalService = (
config, { config, secrets }: ExternalServiceCredentials,
secrets, logger: Logger,
}: ExternalServiceCredentials): ExternalService => { proxySettings?: ProxySettings
): ExternalService => {
const { apiUrl: url, projectKey } = config as JiraPublicConfigurationType; const { apiUrl: url, projectKey } = config as JiraPublicConfigurationType;
const { apiToken, email } = secrets as JiraSecretConfigurationType; const { apiToken, email } = secrets as JiraSecretConfigurationType;
@ -55,6 +58,8 @@ export const createExternalService = ({
const res = await request({ const res = await request({
axios: axiosInstance, axios: axiosInstance,
url: `${incidentUrl}/${id}`, url: `${incidentUrl}/${id}`,
logger,
proxySettings,
}); });
const { fields, ...rest } = res.data; const { fields, ...rest } = res.data;
@ -75,10 +80,12 @@ export const createExternalService = ({
const res = await request<CreateIncidentRequest>({ const res = await request<CreateIncidentRequest>({
axios: axiosInstance, axios: axiosInstance,
url: `${incidentUrl}`, url: `${incidentUrl}`,
logger,
method: 'post', method: 'post',
data: { data: {
fields: { ...incident, project: { key: projectKey }, issuetype: { name: 'Task' } }, fields: { ...incident, project: { key: projectKey }, issuetype: { name: 'Task' } },
}, },
proxySettings,
}); });
const updatedIncident = await getIncident(res.data.id); const updatedIncident = await getIncident(res.data.id);
@ -102,7 +109,9 @@ export const createExternalService = ({
axios: axiosInstance, axios: axiosInstance,
method: 'put', method: 'put',
url: `${incidentUrl}/${incidentId}`, url: `${incidentUrl}/${incidentId}`,
logger,
data: { fields: { ...incident } }, data: { fields: { ...incident } },
proxySettings,
}); });
const updatedIncident = await getIncident(incidentId); const updatedIncident = await getIncident(incidentId);
@ -129,7 +138,9 @@ export const createExternalService = ({
axios: axiosInstance, axios: axiosInstance,
method: 'post', method: 'post',
url: getCommentsURL(incidentId), url: getCommentsURL(incidentId),
logger,
data: { body: comment.comment }, data: { body: comment.comment },
proxySettings,
}); });
return { return {

View file

@ -5,7 +5,11 @@
*/ */
import axios from 'axios'; 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<Logger>;
jest.mock('axios'); jest.mock('axios');
const axiosMock = (axios as unknown) as jest.Mock; 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', () => { describe('request', () => {
beforeEach(() => { beforeEach(() => {
axiosMock.mockImplementation(() => ({ axiosMock.mockImplementation(() => ({
@ -51,9 +35,50 @@ describe('request', () => {
}); });
test('it fetch correctly with defaults', async () => { 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' },
data: { incidentId: '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('/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({ expect(res).toEqual({
status: 200, status: 200,
headers: { 'content-type': 'application/json' }, headers: { 'content-type': 'application/json' },
@ -62,25 +87,24 @@ describe('request', () => {
}); });
test('it fetch correctly', async () => { test('it fetch correctly', async () => {
const res = await request({ axios, url: '/test', method: 'post', data: { id: '123' } }); const res = await request({ axios, url: '/test', method: 'post', logger, data: { id: '123' } });
expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'post', 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({ expect(res).toEqual({
status: 200, status: 200,
headers: { 'content-type': 'application/json' }, headers: { 'content-type': 'application/json' },
data: { incidentId: '123' }, data: { incidentId: '123' },
}); });
}); });
test('it throws correctly', async () => {
axiosMock.mockImplementation(() => ({
status: 404,
headers: { 'content-type': 'application/json' },
data: { incidentId: '123' },
}));
await expect(request({ axios, url: '/test' })).rejects.toThrow();
});
}); });
describe('patch', () => { describe('patch', () => {
@ -92,8 +116,17 @@ describe('patch', () => {
}); });
test('it fetch correctly', async () => { test('it fetch correctly', async () => {
await patch({ axios, url: '/test', data: { id: '123' } }); await patch({ axios, url: '/test', data: { id: '123' }, logger });
expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'patch', data: { id: '123' } }); expect(axiosMock).toHaveBeenCalledWith('/test', {
method: 'patch',
data: { id: '123' },
headers: undefined,
httpAgent: undefined,
httpsAgent: undefined,
params: undefined,
proxy: false,
validateStatus: undefined,
});
}); });
}); });

View file

@ -4,50 +4,68 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { AxiosInstance, Method, AxiosResponse } from 'axios'; import { AxiosInstance, Method, AxiosResponse, AxiosBasicCredentials } from 'axios';
import { Logger } from '../../../../../../src/core/server';
export const throwIfNotAlive = ( import { ProxySettings } from '../../types';
status: number, import { getProxyAgent } from './get_proxy_agent';
contentType: string,
validStatusCodes: number[] = [200, 201, 204]
) => {
if (!validStatusCodes.includes(status) || !contentType.includes('application/json')) {
throw new Error('Instance is not alive.');
}
};
export const request = async <T = unknown>({ export const request = async <T = unknown>({
axios, axios,
url, url,
logger,
method = 'get', method = 'get',
data, data,
params, params,
proxySettings,
headers,
validateStatus,
auth,
}: { }: {
axios: AxiosInstance; axios: AxiosInstance;
url: string; url: string;
logger: Logger;
method?: Method; method?: Method;
data?: T; data?: T;
params?: unknown; params?: unknown;
proxySettings?: ProxySettings;
headers?: Record<string, string> | null;
validateStatus?: (status: number) => boolean;
auth?: AxiosBasicCredentials;
}): Promise<AxiosResponse> => { }): Promise<AxiosResponse> => {
const res = await axios(url, { method, data: data ?? {}, params }); return await axios(url, {
throwIfNotAlive(res.status, res.headers['content-type']); method,
return res; 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 <T = unknown>({ export const patch = async <T = unknown>({
axios, axios,
url, url,
data, data,
logger,
proxySettings,
}: { }: {
axios: AxiosInstance; axios: AxiosInstance;
url: string; url: string;
data: T; data: T;
logger: Logger;
proxySettings?: ProxySettings;
}): Promise<AxiosResponse> => { }): Promise<AxiosResponse> => {
return request({ return request({
axios, axios,
url, url,
logger,
method: 'patch', method: 'patch',
data, data,
proxySettings,
}); });
}; };

View file

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

View file

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

View file

@ -5,22 +5,34 @@
*/ */
import axios, { AxiosResponse } from 'axios'; 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 { interface PostPagerdutyOptions {
apiUrl: string; apiUrl: string;
data: unknown; data: unknown;
headers: Record<string, string>; headers: Record<string, string>;
services: Services; services: Services;
proxySettings?: ProxySettings;
} }
// post an event to pagerduty // post an event to pagerduty
export async function postPagerduty(options: PostPagerdutyOptions): Promise<AxiosResponse> { export async function postPagerduty(
const { apiUrl, data, headers } = options; options: PostPagerdutyOptions,
const axiosOptions = { logger: Logger
): Promise<AxiosResponse> {
const { apiUrl, data, headers, proxySettings } = options;
const axiosInstance = axios.create();
return await request({
axios: axiosInstance,
url: apiUrl,
method: 'post',
logger,
data,
proxySettings,
headers, headers,
validateStatus: () => true, validateStatus: () => true,
}; });
return axios.post(apiUrl, data, axiosOptions);
} }

View file

@ -12,6 +12,7 @@ import { Logger } from '../../../../../../src/core/server';
import { sendEmail } from './send_email'; import { sendEmail } from './send_email';
import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
import nodemailer from 'nodemailer'; import nodemailer from 'nodemailer';
import { ProxySettings } from '../../types';
const createTransportMock = nodemailer.createTransport as jest.Mock; const createTransportMock = nodemailer.createTransport as jest.Mock;
const sendMailMockResult = { result: 'does not matter' }; const sendMailMockResult = { result: 'does not matter' };
@ -63,6 +64,59 @@ describe('send_email module', () => {
}); });
test('handles unauthenticated email using not secure host/port', async () => { 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": "<p>a message</p>
",
"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({ const sendEmailOptions = getSendEmailOptions({
transport: { transport: {
host: 'example.com', host: 'example.com',
@ -80,9 +134,6 @@ describe('send_email module', () => {
"host": "example.com", "host": "example.com",
"port": 1025, "port": 1025,
"secure": false, "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 { return {
content: { content: {
...content, ...content,
@ -181,5 +235,6 @@ function getSendEmailOptions({ content = {}, routing = {}, transport = {} } = {}
user: 'elastic', user: 'elastic',
password: 'changeme', password: 'changeme',
}, },
proxySettings,
}; };
} }

View file

@ -6,10 +6,10 @@
// info on nodemailer: https://nodemailer.com/about/ // info on nodemailer: https://nodemailer.com/about/
import nodemailer from 'nodemailer'; import nodemailer from 'nodemailer';
import { default as MarkdownIt } from 'markdown-it'; import { default as MarkdownIt } from 'markdown-it';
import { Logger } from '../../../../../../src/core/server'; import { Logger } from '../../../../../../src/core/server';
import { ProxySettings } from '../../types';
// an email "service" which doesn't actually send, just returns what it would send // an email "service" which doesn't actually send, just returns what it would send
export const JSON_TRANSPORT_SERVICE = '__json'; export const JSON_TRANSPORT_SERVICE = '__json';
@ -18,6 +18,7 @@ export interface SendEmailOptions {
transport: Transport; transport: Transport;
routing: Routing; routing: Routing;
content: Content; content: Content;
proxySettings?: ProxySettings;
} }
// config validation ensures either service is set or host/port are set // config validation ensures either service is set or host/port are set
@ -44,7 +45,7 @@ export interface Content {
// send an email // send an email
export async function sendEmail(logger: Logger, options: SendEmailOptions): Promise<unknown> { export async function sendEmail(logger: Logger, options: SendEmailOptions): Promise<unknown> {
const { transport, routing, content } = options; const { transport, routing, content, proxySettings } = options;
const { service, host, port, secure, user, password } = transport; const { service, host, port, secure, user, password } = transport;
const { from, to, cc, bcc } = routing; const { from, to, cc, bcc } = routing;
const { subject, message } = content; const { subject, message } = content;
@ -67,11 +68,16 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom
transportConfig.host = host; transportConfig.host = host;
transportConfig.port = port; transportConfig.port = port;
transportConfig.secure = !!secure; transportConfig.secure = !!secure;
if (!transportConfig.secure) { if (proxySettings && !transportConfig.secure) {
transportConfig.tls = { 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); const nodemailerTransport = nodemailer.createTransport(transportConfig);

View file

@ -161,6 +161,7 @@ async function executor(
const secrets = execOptions.secrets; const secrets = execOptions.secrets;
const params = execOptions.params; const params = execOptions.params;
const services = execOptions.services; const services = execOptions.services;
const proxySettings = execOptions.proxySettings;
const apiUrl = getPagerDutyApiUrl(config); const apiUrl = getPagerDutyApiUrl(config);
const headers = { const headers = {
@ -171,7 +172,7 @@ async function executor(
let response; let response;
try { try {
response = await postPagerduty({ apiUrl, data, headers, services }); response = await postPagerduty({ apiUrl, data, headers, services, proxySettings }, logger);
} catch (err) { } catch (err) {
const message = i18n.translate('xpack.actions.builtin.pagerduty.postingErrorMessage', { const message = i18n.translate('xpack.actions.builtin.pagerduty.postingErrorMessage', {
defaultMessage: 'error posting pagerduty event', defaultMessage: 'error posting pagerduty event',

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License. * 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 { createConnector } from '../case/utils';
import { api } from './api'; import { api } from './api';
@ -11,14 +12,25 @@ import { config } from './config';
import { validate } from './validators'; import { validate } from './validators';
import { createExternalService } from './service'; import { createExternalService } from './service';
import { ResilientSecretConfiguration, ResilientPublicConfiguration } from './schema'; import { ResilientSecretConfiguration, ResilientPublicConfiguration } from './schema';
import { ActionsConfigurationUtilities } from '../../actions_config';
import { ActionType } from '../../types';
export const getActionType = createConnector({ export function getActionType({
api, logger,
config, configurationUtilities,
validate, }: {
createExternalService, logger: Logger;
validationSchema: { configurationUtilities: ActionsConfigurationUtilities;
config: ResilientPublicConfiguration, }): ActionType {
secrets: ResilientSecretConfiguration, return createConnector({
}, api,
}); config,
validate,
createExternalService,
validationSchema: {
config: ResilientPublicConfiguration,
secrets: ResilientSecretConfiguration,
},
logger,
})({ configurationUtilities });
}

View file

@ -9,6 +9,9 @@ import axios from 'axios';
import { createExternalService, getValueTextContent, formatUpdateRequest } from './service'; import { createExternalService, getValueTextContent, formatUpdateRequest } from './service';
import * as utils from '../lib/axios_utils'; import * as utils from '../lib/axios_utils';
import { ExternalService } from '../case/types'; 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<Logger>;
jest.mock('axios'); jest.mock('axios');
jest.mock('../lib/axios_utils', () => { jest.mock('../lib/axios_utils', () => {
@ -72,10 +75,13 @@ describe('IBM Resilient service', () => {
let service: ExternalService; let service: ExternalService;
beforeAll(() => { beforeAll(() => {
service = createExternalService({ service = createExternalService(
config: { apiUrl: 'https://resilient.elastic.co', orgId: '201' }, {
secrets: { apiKeyId: 'keyId', apiKeySecret: 'secret' }, config: { apiUrl: 'https://resilient.elastic.co', orgId: '201' },
}); secrets: { apiKeyId: 'keyId', apiKeySecret: 'secret' },
},
logger
);
}); });
afterAll(() => { afterAll(() => {
@ -138,37 +144,49 @@ describe('IBM Resilient service', () => {
describe('createExternalService', () => { describe('createExternalService', () => {
test('throws without url', () => { test('throws without url', () => {
expect(() => expect(() =>
createExternalService({ createExternalService(
config: { apiUrl: null, orgId: '201' }, {
secrets: { apiKeyId: 'token', apiKeySecret: 'secret' }, config: { apiUrl: null, orgId: '201' },
}) secrets: { apiKeyId: 'token', apiKeySecret: 'secret' },
},
logger
)
).toThrow(); ).toThrow();
}); });
test('throws without orgId', () => { test('throws without orgId', () => {
expect(() => expect(() =>
createExternalService({ createExternalService(
config: { apiUrl: 'test.com', orgId: null }, {
secrets: { apiKeyId: 'token', apiKeySecret: 'secret' }, config: { apiUrl: 'test.com', orgId: null },
}) secrets: { apiKeyId: 'token', apiKeySecret: 'secret' },
},
logger
)
).toThrow(); ).toThrow();
}); });
test('throws without username', () => { test('throws without username', () => {
expect(() => expect(() =>
createExternalService({ createExternalService(
config: { apiUrl: 'test.com', orgId: '201' }, {
secrets: { apiKeyId: '', apiKeySecret: 'secret' }, config: { apiUrl: 'test.com', orgId: '201' },
}) secrets: { apiKeyId: '', apiKeySecret: 'secret' },
},
logger
)
).toThrow(); ).toThrow();
}); });
test('throws without password', () => { test('throws without password', () => {
expect(() => expect(() =>
createExternalService({ createExternalService(
config: { apiUrl: 'test.com', orgId: '201' }, {
secrets: { apiKeyId: '', apiKeySecret: undefined }, config: { apiUrl: 'test.com', orgId: '201' },
}) secrets: { apiKeyId: '', apiKeySecret: undefined },
},
logger
)
).toThrow(); ).toThrow();
}); });
}); });
@ -197,6 +215,7 @@ describe('IBM Resilient service', () => {
await service.getIncident('1'); await service.getIncident('1');
expect(requestMock).toHaveBeenCalledWith({ expect(requestMock).toHaveBeenCalledWith({
axios, axios,
logger,
url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1', url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1',
params: { params: {
text_content_output_format: 'objects_convert', text_content_output_format: 'objects_convert',
@ -256,6 +275,7 @@ describe('IBM Resilient service', () => {
expect(requestMock).toHaveBeenCalledWith({ expect(requestMock).toHaveBeenCalledWith({
axios, axios,
url: 'https://resilient.elastic.co/rest/orgs/201/incidents', url: 'https://resilient.elastic.co/rest/orgs/201/incidents',
logger,
method: 'post', method: 'post',
data: { data: {
name: 'title', name: 'title',
@ -311,6 +331,7 @@ describe('IBM Resilient service', () => {
// The second call to the API is the update call. // The second call to the API is the update call.
expect(requestMock.mock.calls[1][0]).toEqual({ expect(requestMock.mock.calls[1][0]).toEqual({
axios, axios,
logger,
method: 'patch', method: 'patch',
url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1', url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1',
data: { data: {
@ -392,7 +413,9 @@ describe('IBM Resilient service', () => {
expect(requestMock).toHaveBeenCalledWith({ expect(requestMock).toHaveBeenCalledWith({
axios, axios,
logger,
method: 'post', method: 'post',
proxySettings: undefined,
url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1/comments', url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1/comments',
data: { data: {
text: { text: {

View file

@ -6,6 +6,7 @@
import axios from 'axios'; import axios from 'axios';
import { Logger } from '../../../../../../src/core/server';
import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types'; import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types';
import { import {
ResilientPublicConfigurationType, ResilientPublicConfigurationType,
@ -19,6 +20,7 @@ import {
import * as i18n from './translations'; import * as i18n from './translations';
import { getErrorMessage, request } from '../lib/axios_utils'; import { getErrorMessage, request } from '../lib/axios_utils';
import { ProxySettings } from '../../types';
const BASE_URL = `rest`; const BASE_URL = `rest`;
const INCIDENT_URL = `incidents`; const INCIDENT_URL = `incidents`;
@ -57,10 +59,11 @@ export const formatUpdateRequest = ({
}; };
}; };
export const createExternalService = ({ export const createExternalService = (
config, { config, secrets }: ExternalServiceCredentials,
secrets, logger: Logger,
}: ExternalServiceCredentials): ExternalService => { proxySettings?: ProxySettings
): ExternalService => {
const { apiUrl: url, orgId } = config as ResilientPublicConfigurationType; const { apiUrl: url, orgId } = config as ResilientPublicConfigurationType;
const { apiKeyId, apiKeySecret } = secrets as ResilientSecretConfigurationType; const { apiKeyId, apiKeySecret } = secrets as ResilientSecretConfigurationType;
@ -88,9 +91,11 @@ export const createExternalService = ({
const res = await request({ const res = await request({
axios: axiosInstance, axios: axiosInstance,
url: `${incidentUrl}/${id}`, url: `${incidentUrl}/${id}`,
logger,
params: { params: {
text_content_output_format: 'objects_convert', text_content_output_format: 'objects_convert',
}, },
proxySettings,
}); });
return { ...res.data, description: res.data.description?.content ?? '' }; return { ...res.data, description: res.data.description?.content ?? '' };
@ -107,6 +112,7 @@ export const createExternalService = ({
axios: axiosInstance, axios: axiosInstance,
url: `${incidentUrl}`, url: `${incidentUrl}`,
method: 'post', method: 'post',
logger,
data: { data: {
...incident, ...incident,
description: { description: {
@ -115,6 +121,7 @@ export const createExternalService = ({
}, },
discovered_date: Date.now(), discovered_date: Date.now(),
}, },
proxySettings,
}); });
return { return {
@ -139,7 +146,9 @@ export const createExternalService = ({
axios: axiosInstance, axios: axiosInstance,
method: 'patch', method: 'patch',
url: `${incidentUrl}/${incidentId}`, url: `${incidentUrl}/${incidentId}`,
logger,
data, data,
proxySettings,
}); });
if (!res.data.success) { if (!res.data.success) {
@ -170,7 +179,9 @@ export const createExternalService = ({
axios: axiosInstance, axios: axiosInstance,
method: 'post', method: 'post',
url: getCommentsURL(incidentId), url: getCommentsURL(incidentId),
logger,
data: { text: { format: 'text', content: comment.comment } }, data: { text: { format: 'text', content: comment.comment } },
proxySettings,
}); });
return { return {

View file

@ -76,10 +76,14 @@ async function executor(
const { subAction, subActionParams } = params; const { subAction, subActionParams } = params;
let data: PushToServiceResponse | null = null; let data: PushToServiceResponse | null = null;
const externalService = createExternalService({ const externalService = createExternalService(
config, {
secrets, config,
}); secrets,
},
logger,
execOptions.proxySettings
);
if (!api[subAction]) { if (!api[subAction]) {
const errorMessage = `[Action][ExternalService] Unsupported subAction type ${subAction}.`; const errorMessage = `[Action][ExternalService] Unsupported subAction type ${subAction}.`;

View file

@ -9,6 +9,9 @@ import axios from 'axios';
import { createExternalService } from './service'; import { createExternalService } from './service';
import * as utils from '../lib/axios_utils'; import * as utils from '../lib/axios_utils';
import { ExternalService } from './types'; 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<Logger>;
jest.mock('axios'); jest.mock('axios');
jest.mock('../lib/axios_utils', () => { jest.mock('../lib/axios_utils', () => {
@ -28,10 +31,13 @@ describe('ServiceNow service', () => {
let service: ExternalService; let service: ExternalService;
beforeAll(() => { beforeAll(() => {
service = createExternalService({ service = createExternalService(
config: { apiUrl: 'https://dev102283.service-now.com' }, {
secrets: { username: 'admin', password: 'admin' }, config: { apiUrl: 'https://dev102283.service-now.com' },
}); secrets: { username: 'admin', password: 'admin' },
},
logger
);
}); });
beforeEach(() => { beforeEach(() => {
@ -41,28 +47,37 @@ describe('ServiceNow service', () => {
describe('createExternalService', () => { describe('createExternalService', () => {
test('throws without url', () => { test('throws without url', () => {
expect(() => expect(() =>
createExternalService({ createExternalService(
config: { apiUrl: null }, {
secrets: { username: 'admin', password: 'admin' }, config: { apiUrl: null },
}) secrets: { username: 'admin', password: 'admin' },
},
logger
)
).toThrow(); ).toThrow();
}); });
test('throws without username', () => { test('throws without username', () => {
expect(() => expect(() =>
createExternalService({ createExternalService(
config: { apiUrl: 'test.com' }, {
secrets: { username: '', password: 'admin' }, config: { apiUrl: 'test.com' },
}) secrets: { username: '', password: 'admin' },
},
logger
)
).toThrow(); ).toThrow();
}); });
test('throws without password', () => { test('throws without password', () => {
expect(() => expect(() =>
createExternalService({ createExternalService(
config: { apiUrl: 'test.com' }, {
secrets: { username: '', password: undefined }, config: { apiUrl: 'test.com' },
}) secrets: { username: '', password: undefined },
},
logger
)
).toThrow(); ).toThrow();
}); });
}); });
@ -84,6 +99,7 @@ describe('ServiceNow service', () => {
await service.getIncident('1'); await service.getIncident('1');
expect(requestMock).toHaveBeenCalledWith({ expect(requestMock).toHaveBeenCalledWith({
axios, axios,
logger,
url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1', url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1',
}); });
}); });
@ -127,6 +143,7 @@ describe('ServiceNow service', () => {
expect(requestMock).toHaveBeenCalledWith({ expect(requestMock).toHaveBeenCalledWith({
axios, axios,
logger,
url: 'https://dev102283.service-now.com/api/now/v2/table/incident', url: 'https://dev102283.service-now.com/api/now/v2/table/incident',
method: 'post', method: 'post',
data: { short_description: 'title', description: 'desc' }, data: { short_description: 'title', description: 'desc' },
@ -179,6 +196,7 @@ describe('ServiceNow service', () => {
expect(patchMock).toHaveBeenCalledWith({ expect(patchMock).toHaveBeenCalledWith({
axios, axios,
logger,
url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1', url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1',
data: { short_description: 'title', description: 'desc' }, data: { short_description: 'title', description: 'desc' },
}); });

View file

@ -9,8 +9,10 @@ import axios from 'axios';
import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from './types'; import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from './types';
import * as i18n from './translations'; import * as i18n from './translations';
import { Logger } from '../../../../../../src/core/server';
import { ServiceNowPublicConfigurationType, ServiceNowSecretConfigurationType } from './types'; import { ServiceNowPublicConfigurationType, ServiceNowSecretConfigurationType } from './types';
import { request, getErrorMessage, addTimeZoneToDate, patch } from '../lib/axios_utils'; import { request, getErrorMessage, addTimeZoneToDate, patch } from '../lib/axios_utils';
import { ProxySettings } from '../../types';
const API_VERSION = 'v2'; const API_VERSION = 'v2';
const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`; 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 // 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=`; const VIEW_INCIDENT_URL = `nav_to.do?uri=incident.do?sys_id=`;
export const createExternalService = ({ export const createExternalService = (
config, { config, secrets }: ExternalServiceCredentials,
secrets, logger: Logger,
}: ExternalServiceCredentials): ExternalService => { proxySettings?: ProxySettings
): ExternalService => {
const { apiUrl: url } = config as ServiceNowPublicConfigurationType; const { apiUrl: url } = config as ServiceNowPublicConfigurationType;
const { username, password } = secrets as ServiceNowSecretConfigurationType; const { username, password } = secrets as ServiceNowSecretConfigurationType;
@ -43,6 +46,8 @@ export const createExternalService = ({
const res = await request({ const res = await request({
axios: axiosInstance, axios: axiosInstance,
url: `${incidentUrl}/${id}`, url: `${incidentUrl}/${id}`,
logger,
proxySettings,
}); });
return { ...res.data.result }; return { ...res.data.result };
@ -58,6 +63,8 @@ export const createExternalService = ({
const res = await request({ const res = await request({
axios: axiosInstance, axios: axiosInstance,
url: incidentUrl, url: incidentUrl,
logger,
proxySettings,
params, params,
}); });
@ -71,9 +78,13 @@ export const createExternalService = ({
const createIncident = async ({ incident }: ExternalServiceParams) => { const createIncident = async ({ incident }: ExternalServiceParams) => {
try { try {
logger.warn(`incident error : ${JSON.stringify(proxySettings)}`);
logger.warn(`incident error : ${url}`);
const res = await request({ const res = await request({
axios: axiosInstance, axios: axiosInstance,
url: `${incidentUrl}`, url: `${incidentUrl}`,
logger,
proxySettings,
method: 'post', method: 'post',
data: { ...(incident as Record<string, unknown>) }, data: { ...(incident as Record<string, unknown>) },
}); });
@ -96,7 +107,9 @@ export const createExternalService = ({
const res = await patch({ const res = await patch({
axios: axiosInstance, axios: axiosInstance,
url: `${incidentUrl}/${incidentId}`, url: `${incidentUrl}/${incidentId}`,
logger,
data: { ...(incident as Record<string, unknown>) }, data: { ...(incident as Record<string, unknown>) },
proxySettings,
}); });
return { return {

View file

@ -4,25 +4,40 @@
* you may not use this file except in compliance with the Elastic License. * 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 { Services, ActionTypeExecutorResult } from '../types';
import { validateParams, validateSecrets } from '../lib'; import { validateParams, validateSecrets } from '../lib';
import { getActionType, SlackActionType, SlackActionTypeExecutorOptions } from './slack'; import { getActionType, SlackActionType, SlackActionTypeExecutorOptions } from './slack';
import { actionsConfigMock } from '../actions_config.mock'; import { actionsConfigMock } from '../actions_config.mock';
import { actionsMock } from '../mocks'; 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 ACTION_TYPE_ID = '.slack';
const services: Services = actionsMock.createServices(); const services: Services = actionsMock.createServices();
let actionType: SlackActionType; let actionType: SlackActionType;
let mockedLogger: jest.Mocked<Logger>;
beforeAll(() => { beforeAll(() => {
const { logger } = createActionTypeRegistry();
actionType = getActionType({ actionType = getActionType({
async executor(options) { async executor(options) {
return { status: 'ok', actionId: options.actionId }; return { status: 'ok', actionId: options.actionId };
}, },
configurationUtilities: actionsConfigMock.create(), configurationUtilities: actionsConfigMock.create(),
logger,
}); });
mockedLogger = logger;
expect(actionType).toBeTruthy();
}); });
describe('action registeration', () => { describe('action registeration', () => {
@ -83,6 +98,7 @@ describe('validateActionTypeSecrets()', () => {
test('should validate and pass when the slack webhookUrl is whitelisted', () => { test('should validate and pass when the slack webhookUrl is whitelisted', () => {
actionType = getActionType({ actionType = getActionType({
logger: mockedLogger,
configurationUtilities: { configurationUtilities: {
...actionsConfigMock.create(), ...actionsConfigMock.create(),
ensureWhitelistedUri: (url) => { ensureWhitelistedUri: (url) => {
@ -98,9 +114,10 @@ describe('validateActionTypeSecrets()', () => {
test('config validation returns an error if the specified URL isnt whitelisted', () => { test('config validation returns an error if the specified URL isnt whitelisted', () => {
actionType = getActionType({ actionType = getActionType({
logger: mockedLogger,
configurationUtilities: { configurationUtilities: {
...actionsConfigMock.create(), ...actionsConfigMock.create(),
ensureWhitelistedHostname: (url) => { ensureWhitelistedHostname: () => {
throw new Error(`target hostname is not whitelisted`); throw new Error(`target hostname is not whitelisted`);
}, },
}, },
@ -136,6 +153,7 @@ describe('execute()', () => {
actionType = getActionType({ actionType = getActionType({
executor: mockSlackExecutor, executor: mockSlackExecutor,
logger: mockedLogger,
configurationUtilities: actionsConfigMock.create(), configurationUtilities: actionsConfigMock.create(),
}); });
}); });
@ -147,6 +165,10 @@ describe('execute()', () => {
config: {}, config: {},
secrets: { webhookUrl: 'http://example.com' }, secrets: { webhookUrl: 'http://example.com' },
params: { message: 'this invocation should succeed' }, params: { message: 'this invocation should succeed' },
proxySettings: {
proxyUrl: 'https://someproxyhost',
rejectUnauthorizedCertificates: false,
},
}); });
expect(response).toMatchInlineSnapshot(` expect(response).toMatchInlineSnapshot(`
Object { Object {
@ -170,4 +192,25 @@ describe('execute()', () => {
`"slack mockExecutor failure: this invocation should fail"` `"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'
);
});
}); });

View file

@ -6,11 +6,14 @@
import { URL } from 'url'; import { URL } from 'url';
import { curry } from 'lodash'; import { curry } from 'lodash';
import { HttpsProxyAgent } from 'https-proxy-agent';
import HttpProxyAgent from 'http-proxy-agent';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { schema, TypeOf } from '@kbn/config-schema'; import { schema, TypeOf } from '@kbn/config-schema';
import { IncomingWebhook, IncomingWebhookResult } from '@slack/webhook'; import { IncomingWebhook, IncomingWebhookResult } from '@slack/webhook';
import { pipe } from 'fp-ts/lib/pipeable'; import { pipe } from 'fp-ts/lib/pipeable';
import { map, getOrElse } from 'fp-ts/lib/Option'; import { map, getOrElse } from 'fp-ts/lib/Option';
import { Logger } from '../../../../../src/core/server';
import { getRetryAfterIntervalFromHeaders } from './lib/http_rersponse_retry_header'; import { getRetryAfterIntervalFromHeaders } from './lib/http_rersponse_retry_header';
import { import {
@ -20,6 +23,7 @@ import {
ExecutorType, ExecutorType,
} from '../types'; } from '../types';
import { ActionsConfigurationUtilities } from '../actions_config'; import { ActionsConfigurationUtilities } from '../actions_config';
import { getProxyAgent } from './lib/get_proxy_agent';
export type SlackActionType = ActionType<{}, ActionTypeSecretsType, ActionParamsType, unknown>; export type SlackActionType = ActionType<{}, ActionTypeSecretsType, ActionParamsType, unknown>;
export type SlackActionTypeExecutorOptions = ActionTypeExecutorOptions< export type SlackActionTypeExecutorOptions = ActionTypeExecutorOptions<
@ -49,9 +53,11 @@ const ParamsSchema = schema.object({
// customizing executor is only used for tests // customizing executor is only used for tests
export function getActionType({ export function getActionType({
logger,
configurationUtilities, configurationUtilities,
executor = slackExecutor, executor = curry(slackExecutor)({ logger }),
}: { }: {
logger: Logger;
configurationUtilities: ActionsConfigurationUtilities; configurationUtilities: ActionsConfigurationUtilities;
executor?: ExecutorType<{}, ActionTypeSecretsType, ActionParamsType, unknown>; executor?: ExecutorType<{}, ActionTypeSecretsType, ActionParamsType, unknown>;
}): SlackActionType { }): SlackActionType {
@ -99,6 +105,7 @@ function valdiateActionTypeConfig(
// action executor // action executor
async function slackExecutor( async function slackExecutor(
{ logger }: { logger: Logger },
execOptions: SlackActionTypeExecutorOptions execOptions: SlackActionTypeExecutorOptions
): Promise<ActionTypeExecutorResult<unknown>> { ): Promise<ActionTypeExecutorResult<unknown>> {
const actionId = execOptions.actionId; const actionId = execOptions.actionId;
@ -109,10 +116,22 @@ async function slackExecutor(
const { webhookUrl } = secrets; const { webhookUrl } = secrets;
const { message } = params; 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 { 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); result = await webhook.send(message);
} catch (err) { } catch (err) {
logger.error(`error on ${actionId} slack event: ${err.message}`);
if (err.original == null || err.original.response == null) { if (err.original == null || err.original.response == null) {
return serviceErrorResult(actionId, err.message); return serviceErrorResult(actionId, err.message);
} }
@ -143,6 +162,8 @@ async function slackExecutor(
}, },
} }
); );
logger.error(`error on ${actionId} slack action: ${errMessage}`);
return errorResult(actionId, errMessage); return errorResult(actionId, errMessage);
} }

View file

@ -4,10 +4,6 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
jest.mock('axios', () => ({
request: jest.fn(),
}));
import { Services } from '../types'; import { Services } from '../types';
import { validateConfig, validateSecrets, validateParams } from '../lib'; import { validateConfig, validateSecrets, validateParams } from '../lib';
import { actionsConfigMock } from '../actions_config.mock'; import { actionsConfigMock } from '../actions_config.mock';
@ -24,7 +20,22 @@ import {
WebhookMethods, WebhookMethods,
} from './webhook'; } 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'; const ACTION_TYPE_ID = '.webhook';
@ -227,7 +238,7 @@ describe('params validation', () => {
describe('execute()', () => { describe('execute()', () => {
beforeAll(() => { beforeAll(() => {
axiosRequestMock.mockReset(); requestMock.mockReset();
actionType = getActionType({ actionType = getActionType({
logger: mockedLogger, logger: mockedLogger,
configurationUtilities: actionsConfigMock.create(), configurationUtilities: actionsConfigMock.create(),
@ -235,8 +246,8 @@ describe('execute()', () => {
}); });
beforeEach(() => { beforeEach(() => {
axiosRequestMock.mockReset(); requestMock.mockReset();
axiosRequestMock.mockResolvedValue({ requestMock.mockResolvedValue({
status: 200, status: 200,
statusText: '', statusText: '',
data: '', data: '',
@ -261,17 +272,42 @@ describe('execute()', () => {
params: { body: 'some data' }, params: { body: 'some data' },
}); });
expect(axiosRequestMock.mock.calls[0][0]).toMatchInlineSnapshot(` expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(`
Object { Object {
"auth": Object { "auth": Object {
"password": "123", "password": "123",
"username": "abc", "username": "abc",
}, },
"axios": undefined,
"data": "some data", "data": "some data",
"headers": Object { "headers": Object {
"aheader": "a value", "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", "method": "post",
"proxySettings": undefined,
"url": "https://abc.def/my-webhook", "url": "https://abc.def/my-webhook",
} }
`); `);
@ -294,13 +330,38 @@ describe('execute()', () => {
params: { body: 'some data' }, params: { body: 'some data' },
}); });
expect(axiosRequestMock.mock.calls[0][0]).toMatchInlineSnapshot(` expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(`
Object { Object {
"axios": undefined,
"data": "some data", "data": "some data",
"headers": Object { "headers": Object {
"aheader": "a value", "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", "method": "post",
"proxySettings": undefined,
"url": "https://abc.def/my-webhook", "url": "https://abc.def/my-webhook",
} }
`); `);

View file

@ -15,6 +15,7 @@ import { isOk, promiseResult, Result } from './lib/result_type';
import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types';
import { ActionsConfigurationUtilities } from '../actions_config'; import { ActionsConfigurationUtilities } from '../actions_config';
import { Logger } from '../../../../../src/core/server'; import { Logger } from '../../../../../src/core/server';
import { request } from './lib/axios_utils';
// config definition // config definition
export enum WebhookMethods { export enum WebhookMethods {
@ -136,13 +137,18 @@ export async function executor(
? { auth: { username: secrets.user, password: secrets.password } } ? { auth: { username: secrets.user, password: secrets.password } }
: {}; : {};
const axiosInstance = axios.create();
const result: Result<AxiosResponse, AxiosError> = await promiseResult( const result: Result<AxiosResponse, AxiosError> = await promiseResult(
axios.request({ request({
axios: axiosInstance,
method, method,
url, url,
logger,
...basicAuth, ...basicAuth,
headers, headers,
data, data,
proxySettings: execOptions.proxySettings,
}) })
); );
@ -159,7 +165,7 @@ export async function executor(
if (error.response) { if (error.response) {
const { status, statusText, headers: responseHeaders } = error.response; const { status, statusText, headers: responseHeaders } = error.response;
const message = `[${status}] ${statusText}`; 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 // The request was made and the server responded with a status code
// that falls out of the range of 2xx // that falls out of the range of 2xx
// special handling for 5xx // special handling for 5xx
@ -178,7 +184,7 @@ export async function executor(
return errorResultInvalid(actionId, message); 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); return errorResultUnexpectedError(actionId);
} }
} }

View file

@ -15,6 +15,7 @@ describe('config validation', () => {
"*", "*",
], ],
"preconfigured": Object {}, "preconfigured": Object {},
"rejectUnauthorizedCertificates": true,
"whitelistedHosts": Array [ "whitelistedHosts": Array [
"*", "*",
], ],
@ -33,6 +34,7 @@ describe('config validation', () => {
}, },
}, },
}, },
rejectUnauthorizedCertificates: false,
}; };
expect(configSchema.validate(config)).toMatchInlineSnapshot(` expect(configSchema.validate(config)).toMatchInlineSnapshot(`
Object { Object {
@ -50,6 +52,7 @@ describe('config validation', () => {
"secrets": Object {}, "secrets": Object {},
}, },
}, },
"rejectUnauthorizedCertificates": false,
"whitelistedHosts": Array [ "whitelistedHosts": Array [
"*", "*",
], ],

View file

@ -32,6 +32,9 @@ export const configSchema = schema.object({
defaultValue: {}, defaultValue: {},
validate: validatePreconfigured, 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<typeof configSchema>; export type ActionsConfig = TypeOf<typeof configSchema>;

View file

@ -12,6 +12,7 @@ import {
GetServicesFunction, GetServicesFunction,
RawAction, RawAction,
PreConfiguredAction, PreConfiguredAction,
ProxySettings,
} from '../types'; } from '../types';
import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server';
import { SpacesServiceSetup } from '../../../spaces/server'; import { SpacesServiceSetup } from '../../../spaces/server';
@ -28,6 +29,7 @@ export interface ActionExecutorContext {
actionTypeRegistry: ActionTypeRegistryContract; actionTypeRegistry: ActionTypeRegistryContract;
eventLogger: IEventLogger; eventLogger: IEventLogger;
preconfiguredActions: PreConfiguredAction[]; preconfiguredActions: PreConfiguredAction[];
proxySettings?: ProxySettings;
} }
export interface ExecuteOptions { export interface ExecuteOptions {
@ -78,6 +80,7 @@ export class ActionExecutor {
eventLogger, eventLogger,
preconfiguredActions, preconfiguredActions,
getActionsClientWithRequest, getActionsClientWithRequest,
proxySettings,
} = this.actionExecutorContext!; } = this.actionExecutorContext!;
const services = getServices(request); const services = getServices(request);
@ -133,6 +136,7 @@ export class ActionExecutor {
params: validatedParams, params: validatedParams,
config: validatedConfig, config: validatedConfig,
secrets: validatedSecrets, secrets: validatedSecrets,
proxySettings,
}); });
} catch (err) { } catch (err) {
rawResult = { rawResult = {

View file

@ -34,6 +34,7 @@ describe('Actions Plugin', () => {
enabledActionTypes: ['*'], enabledActionTypes: ['*'],
whitelistedHosts: ['*'], whitelistedHosts: ['*'],
preconfigured: {}, preconfigured: {},
rejectUnauthorizedCertificates: true,
}); });
plugin = new ActionsPlugin(context); plugin = new ActionsPlugin(context);
coreSetup = coreMock.createSetup(); coreSetup = coreMock.createSetup();
@ -194,6 +195,7 @@ describe('Actions Plugin', () => {
secrets: {}, secrets: {},
}, },
}, },
rejectUnauthorizedCertificates: true,
}); });
plugin = new ActionsPlugin(context); plugin = new ActionsPlugin(context);
coreSetup = coreMock.createSetup(); coreSetup = coreMock.createSetup();
@ -217,7 +219,7 @@ describe('Actions Plugin', () => {
// coreMock.createSetup doesn't support Plugin generics // coreMock.createSetup doesn't support Plugin generics
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
await plugin.setup(coreSetup as any, pluginsSetup); 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); expect(pluginStart.isActionExecutable('preconfiguredServerLog', '.server-log')).toBe(true);
}); });
@ -232,7 +234,7 @@ describe('Actions Plugin', () => {
usingEphemeralEncryptionKey: false, usingEphemeralEncryptionKey: false,
}, },
}); });
const pluginStart = plugin.start(coreStart, pluginsStart); const pluginStart = await plugin.start(coreStart, pluginsStart);
await pluginStart.getActionsClientWithRequest(httpServerMock.createKibanaRequest()); await pluginStart.getActionsClientWithRequest(httpServerMock.createKibanaRequest());
}); });
@ -241,7 +243,7 @@ describe('Actions Plugin', () => {
// coreMock.createSetup doesn't support Plugin generics // coreMock.createSetup doesn't support Plugin generics
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
await plugin.setup(coreSetup as any, pluginsSetup); 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); expect(pluginsSetup.encryptedSavedObjects.usingEphemeralEncryptionKey).toEqual(true);
await expect( await expect(

View file

@ -116,6 +116,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
private readonly config: Promise<ActionsConfig>; private readonly config: Promise<ActionsConfig>;
private readonly logger: Logger; private readonly logger: Logger;
private actionsConfig?: ActionsConfig;
private serverBasePath?: string; private serverBasePath?: string;
private taskRunnerFactory?: TaskRunnerFactory; private taskRunnerFactory?: TaskRunnerFactory;
private actionTypeRegistry?: ActionTypeRegistry; private actionTypeRegistry?: ActionTypeRegistry;
@ -173,12 +174,12 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
// get executions count // get executions count
const taskRunnerFactory = new TaskRunnerFactory(actionExecutor); const taskRunnerFactory = new TaskRunnerFactory(actionExecutor);
const actionsConfig = (await this.config) as ActionsConfig; this.actionsConfig = (await this.config) as ActionsConfig;
const actionsConfigUtils = getActionsConfigurationUtilities(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({ this.preconfiguredActions.push({
...actionsConfig.preconfigured[preconfiguredId], ...this.actionsConfig.preconfigured[preconfiguredId],
id: preconfiguredId, id: preconfiguredId,
isPreconfigured: true, isPreconfigured: true,
}); });
@ -317,6 +318,14 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
encryptedSavedObjectsClient, encryptedSavedObjectsClient,
actionTypeRegistry: actionTypeRegistry!, actionTypeRegistry: actionTypeRegistry!,
preconfiguredActions, preconfiguredActions,
proxySettings:
this.actionsConfig && this.actionsConfig.proxyUrl
? {
proxyUrl: this.actionsConfig.proxyUrl,
proxyHeaders: this.actionsConfig.proxyHeaders,
rejectUnauthorizedCertificates: this.actionsConfig.rejectUnauthorizedCertificates,
}
: undefined,
}); });
taskRunnerFactory!.initialize({ taskRunnerFactory!.initialize({

View file

@ -58,6 +58,7 @@ export interface ActionTypeExecutorOptions<Config, Secrets, Params> {
config: Config; config: Config;
secrets: Secrets; secrets: Secrets;
params: Params; params: Params;
proxySettings?: ProxySettings;
} }
export interface ActionResult<Config extends ActionTypeConfig = ActionTypeConfig> { export interface ActionResult<Config extends ActionTypeConfig = ActionTypeConfig> {
@ -140,3 +141,9 @@ export interface ActionTaskExecutorParams {
spaceId: string; spaceId: string;
actionTaskParamsId: string; actionTaskParamsId: string;
} }
export interface ProxySettings {
proxyUrl: string;
proxyHeaders?: Record<string, string>;
rejectUnauthorizedCertificates: boolean;
}

View file

@ -11,4 +11,5 @@ export default createTestConfig('basic', {
disabledPlugins: [], disabledPlugins: [],
license: 'basic', license: 'basic',
ssl: true, ssl: true,
enableActionsProxy: false,
}); });

View file

@ -5,6 +5,7 @@
*/ */
import path from 'path'; import path from 'path';
import getPort from 'get-port';
import fs from 'fs'; import fs from 'fs';
import { CA_CERT_PATH } from '@kbn/dev-utils'; import { CA_CERT_PATH } from '@kbn/dev-utils';
import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
@ -15,6 +16,7 @@ interface CreateTestConfigOptions {
license: string; license: string;
disabledPlugins?: string[]; disabledPlugins?: string[];
ssl?: boolean; ssl?: boolean;
enableActionsProxy: boolean;
} }
// test.not-enabled is specifically not enabled // 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() fs.statSync(path.resolve(__dirname, 'fixtures', 'plugins', file)).isDirectory()
); );
const actionsProxyUrl = options.enableActionsProxy
? [`--xpack.actions.proxyUrl=http://localhost:${await getPort()}`]
: [];
return { return {
testFiles: [require.resolve(`../${name}/tests/`)], testFiles: [require.resolve(`../${name}/tests/`)],
servers, servers,
@ -85,6 +91,9 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
])}`, ])}`,
'--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', '--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"',
`--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`,
...actionsProxyUrl,
'--xpack.actions.rejectUnauthorizedCertificates=false',
'--xpack.eventLog.logEntries=true', '--xpack.eventLog.logEntries=true',
`--xpack.actions.preconfigured=${JSON.stringify({ `--xpack.actions.preconfigured=${JSON.stringify({
'my-slack1': { 'my-slack1': {

View file

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

View file

@ -11,4 +11,5 @@ export default createTestConfig('security_and_spaces', {
disabledPlugins: [], disabledPlugins: [],
license: 'trial', license: 'trial',
ssl: true, ssl: true,
enableActionsProxy: true,
}); });

View file

@ -6,6 +6,7 @@
import expect from '@kbn/expect'; import expect from '@kbn/expect';
import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server';
import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { FtrProviderContext } from '../../../../common/ftr_provider_context';
import { import {
@ -35,6 +36,7 @@ const mapping = [
export default function jiraTest({ getService }: FtrProviderContext) { export default function jiraTest({ getService }: FtrProviderContext) {
const supertest = getService('supertest'); const supertest = getService('supertest');
const kibanaServer = getService('kibanaServer'); const kibanaServer = getService('kibanaServer');
const config = getService('config');
const mockJira = { const mockJira = {
config: { config: {
@ -73,12 +75,19 @@ export default function jiraTest({ getService }: FtrProviderContext) {
}; };
let jiraSimulatorURL: string = '<could not determine kibana url>'; let jiraSimulatorURL: string = '<could not determine kibana url>';
let proxyServer: any;
let proxyHaveBeenCalled = false;
describe('Jira', () => { describe('Jira', () => {
before(() => { before(() => {
jiraSimulatorURL = kibanaServer.resolveUrl( jiraSimulatorURL = kibanaServer.resolveUrl(
getExternalServiceSimulatorPath(ExternalServiceSimulator.JIRA) 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', () => { describe('Jira - Action Creation', () => {
@ -529,6 +538,8 @@ export default function jiraTest({ getService }: FtrProviderContext) {
}) })
.expect(200); .expect(200);
expect(proxyHaveBeenCalled).to.equal(true);
expect(body).to.eql({ expect(body).to.eql({
status: 'ok', status: 'ok',
actionId: simulatedActionId, actionId: simulatedActionId,
@ -542,5 +553,9 @@ export default function jiraTest({ getService }: FtrProviderContext) {
}); });
}); });
}); });
after(() => {
proxyServer.close();
});
}); });
} }

View file

@ -6,6 +6,7 @@
import expect from '@kbn/expect'; import expect from '@kbn/expect';
import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server';
import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { FtrProviderContext } from '../../../../common/ftr_provider_context';
import { import {
@ -17,16 +18,25 @@ import {
export default function pagerdutyTest({ getService }: FtrProviderContext) { export default function pagerdutyTest({ getService }: FtrProviderContext) {
const supertest = getService('supertest'); const supertest = getService('supertest');
const kibanaServer = getService('kibanaServer'); const kibanaServer = getService('kibanaServer');
const config = getService('config');
describe('pagerduty action', () => { describe('pagerduty action', () => {
let simulatedActionId = ''; let simulatedActionId = '';
let pagerdutySimulatorURL: string = '<could not determine kibana url>'; let pagerdutySimulatorURL: string = '<could not determine kibana url>';
let proxyServer: any;
let proxyHaveBeenCalled = false;
// need to wait for kibanaServer to settle ... // need to wait for kibanaServer to settle ...
before(() => { before(() => {
pagerdutySimulatorURL = kibanaServer.resolveUrl( pagerdutySimulatorURL = kibanaServer.resolveUrl(
getExternalServiceSimulatorPath(ExternalServiceSimulator.PAGERDUTY) 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 () => { it('should return successfully when passed valid create parameters', async () => {
@ -144,6 +154,8 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) {
}, },
}) })
.expect(200); .expect(200);
expect(proxyHaveBeenCalled).to.equal(true);
expect(result).to.eql({ expect(result).to.eql({
status: 'ok', status: 'ok',
actionId: simulatedActionId, 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.message).to.match(/error posting pagerduty event: http status 502/);
expect(result.retry).to.equal(true); expect(result.retry).to.equal(true);
}); });
after(() => {
proxyServer.close();
});
}); });
} }

View file

@ -6,6 +6,7 @@
import expect from '@kbn/expect'; import expect from '@kbn/expect';
import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server';
import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { FtrProviderContext } from '../../../../common/ftr_provider_context';
import { import {
@ -35,6 +36,7 @@ const mapping = [
export default function resilientTest({ getService }: FtrProviderContext) { export default function resilientTest({ getService }: FtrProviderContext) {
const supertest = getService('supertest'); const supertest = getService('supertest');
const kibanaServer = getService('kibanaServer'); const kibanaServer = getService('kibanaServer');
const config = getService('config');
const mockResilient = { const mockResilient = {
config: { config: {
@ -73,12 +75,19 @@ export default function resilientTest({ getService }: FtrProviderContext) {
}; };
let resilientSimulatorURL: string = '<could not determine kibana url>'; let resilientSimulatorURL: string = '<could not determine kibana url>';
let proxyServer: any;
let proxyHaveBeenCalled = false;
describe('IBM Resilient', () => { describe('IBM Resilient', () => {
before(() => { before(() => {
resilientSimulatorURL = kibanaServer.resolveUrl( resilientSimulatorURL = kibanaServer.resolveUrl(
getExternalServiceSimulatorPath(ExternalServiceSimulator.RESILIENT) 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', () => { describe('IBM Resilient - Action Creation', () => {
@ -529,6 +538,8 @@ export default function resilientTest({ getService }: FtrProviderContext) {
}) })
.expect(200); .expect(200);
expect(proxyHaveBeenCalled).to.equal(true);
expect(body).to.eql({ expect(body).to.eql({
status: 'ok', status: 'ok',
actionId: simulatedActionId, actionId: simulatedActionId,
@ -542,5 +553,9 @@ export default function resilientTest({ getService }: FtrProviderContext) {
}); });
}); });
}); });
after(() => {
proxyServer.close();
});
}); });
} }

View file

@ -6,6 +6,7 @@
import expect from '@kbn/expect'; import expect from '@kbn/expect';
import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server';
import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { FtrProviderContext } from '../../../../common/ftr_provider_context';
import { import {
@ -35,6 +36,7 @@ const mapping = [
export default function servicenowTest({ getService }: FtrProviderContext) { export default function servicenowTest({ getService }: FtrProviderContext) {
const supertest = getService('supertest'); const supertest = getService('supertest');
const kibanaServer = getService('kibanaServer'); const kibanaServer = getService('kibanaServer');
const config = getService('config');
const mockServiceNow = { const mockServiceNow = {
config: { config: {
@ -72,12 +74,20 @@ export default function servicenowTest({ getService }: FtrProviderContext) {
}; };
let servicenowSimulatorURL: string = '<could not determine kibana url>'; let servicenowSimulatorURL: string = '<could not determine kibana url>';
let proxyServer: any;
let proxyHaveBeenCalled = false;
describe('ServiceNow', () => { describe('ServiceNow', () => {
before(() => { before(() => {
servicenowSimulatorURL = kibanaServer.resolveUrl( servicenowSimulatorURL = kibanaServer.resolveUrl(
getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) 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', () => { describe('ServiceNow - Action Creation', () => {
@ -448,6 +458,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) {
}, },
}) })
.expect(200); .expect(200);
expect(proxyHaveBeenCalled).to.equal(true);
expect(result).to.eql({ expect(result).to.eql({
status: 'ok', status: 'ok',
@ -462,5 +473,9 @@ export default function servicenowTest({ getService }: FtrProviderContext) {
}); });
}); });
}); });
after(() => {
proxyServer.close();
});
}); });
} }

View file

@ -7,6 +7,7 @@
import expect from '@kbn/expect'; import expect from '@kbn/expect';
import http from 'http'; import http from 'http';
import getPort from 'get-port'; import getPort from 'get-port';
import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server';
import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { FtrProviderContext } from '../../../../common/ftr_provider_context';
import { getSlackServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; 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 // eslint-disable-next-line import/no-default-export
export default function slackTest({ getService }: FtrProviderContext) { export default function slackTest({ getService }: FtrProviderContext) {
const supertest = getService('supertest'); const supertest = getService('supertest');
const config = getService('config');
describe('slack action', () => { describe('slack action', () => {
let simulatedActionId = ''; let simulatedActionId = '';
let slackSimulatorURL: string = ''; let slackSimulatorURL: string = '';
let slackServer: http.Server; let slackServer: http.Server;
let proxyServer: any;
let proxyHaveBeenCalled = false;
// need to wait for kibanaServer to settle ... // need to wait for kibanaServer to settle ...
before(async () => { before(async () => {
slackServer = await getSlackServer(); slackServer = await getSlackServer();
const availablePort = await getPort({ port: 9000 }); const availablePort = await getPort({ port: 9000 });
slackServer.listen(availablePort); slackServer.listen(availablePort);
slackSimulatorURL = `http://localhost:${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 () => { it('should return 200 when creating a slack action successfully', async () => {
@ -155,6 +165,7 @@ export default function slackTest({ getService }: FtrProviderContext) {
}) })
.expect(200); .expect(200);
expect(result.status).to.eql('ok'); expect(result.status).to.eql('ok');
expect(proxyHaveBeenCalled).to.equal(true);
}); });
it('should handle an empty message error', async () => { it('should handle an empty message error', async () => {
@ -222,6 +233,7 @@ export default function slackTest({ getService }: FtrProviderContext) {
after(() => { after(() => {
slackServer.close(); slackServer.close();
proxyServer.close();
}); });
}); });
} }

View file

@ -8,6 +8,7 @@ import http from 'http';
import getPort from 'get-port'; import getPort from 'get-port';
import expect from '@kbn/expect'; import expect from '@kbn/expect';
import { URL, format as formatUrl } from 'url'; import { URL, format as formatUrl } from 'url';
import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server';
import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { FtrProviderContext } from '../../../../common/ftr_provider_context';
import { import {
getExternalServiceSimulatorPath, getExternalServiceSimulatorPath,
@ -31,6 +32,7 @@ function parsePort(url: Record<string, string>): Record<string, string | null |
export default function webhookTest({ getService }: FtrProviderContext) { export default function webhookTest({ getService }: FtrProviderContext) {
const supertest = getService('supertest'); const supertest = getService('supertest');
const kibanaServer = getService('kibanaServer'); const kibanaServer = getService('kibanaServer');
const configService = getService('config');
async function createWebhookAction( async function createWebhookAction(
webhookSimulatorURL: string, webhookSimulatorURL: string,
@ -69,6 +71,8 @@ export default function webhookTest({ getService }: FtrProviderContext) {
let webhookSimulatorURL: string = ''; let webhookSimulatorURL: string = '';
let webhookServer: http.Server; let webhookServer: http.Server;
let kibanaURL: string = '<could not determine kibana url>'; let kibanaURL: string = '<could not determine kibana url>';
let proxyServer: any;
let proxyHaveBeenCalled = false;
// need to wait for kibanaServer to settle ... // need to wait for kibanaServer to settle ...
before(async () => { before(async () => {
webhookServer = await getWebhookServer(); webhookServer = await getWebhookServer();
@ -76,6 +80,12 @@ export default function webhookTest({ getService }: FtrProviderContext) {
webhookServer.listen(availablePort); webhookServer.listen(availablePort);
webhookSimulatorURL = `http://localhost:${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( kibanaURL = kibanaServer.resolveUrl(
getExternalServiceSimulatorPath(ExternalServiceSimulator.WEBHOOK) getExternalServiceSimulatorPath(ExternalServiceSimulator.WEBHOOK)
); );
@ -140,6 +150,7 @@ export default function webhookTest({ getService }: FtrProviderContext) {
.expect(200); .expect(200);
expect(result.status).to.eql('ok'); expect(result.status).to.eql('ok');
expect(proxyHaveBeenCalled).to.equal(true);
}); });
it('should support the POST method against webhook target', async () => { it('should support the POST method against webhook target', async () => {
@ -218,7 +229,7 @@ export default function webhookTest({ getService }: FtrProviderContext) {
.expect(200); .expect(200);
expect(result.status).to.eql('error'); 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 () => { it('should handle failing webhook targets', async () => {
@ -240,6 +251,7 @@ export default function webhookTest({ getService }: FtrProviderContext) {
after(() => { after(() => {
webhookServer.close(); webhookServer.close();
proxyServer.close();
}); });
}); });
} }

View file

@ -7,4 +7,8 @@
import { createTestConfig } from '../common/config'; import { createTestConfig } from '../common/config';
// eslint-disable-next-line import/no-default-export // 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,
});

View file

@ -3913,6 +3913,20 @@
resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz#9140779736aa2655635ee756e2467d787cfe8a2a" resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz#9140779736aa2655635ee756e2467d787cfe8a2a"
integrity sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A== 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": "@types/inert@^5.1.2":
version "5.1.2" version "5.1.2"
resolved "https://registry.yarnpkg.com/@types/inert/-/inert-5.1.2.tgz#2bb8bef3b2462f904c960654c9edfa39285a85c6" resolved "https://registry.yarnpkg.com/@types/inert/-/inert-5.1.2.tgz#2bb8bef3b2462f904c960654c9edfa39285a85c6"