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:
parent
64b8b88c64
commit
52bd6d98ea
|
@ -44,9 +44,9 @@
|
|||
"@storybook/addon-storyshots": "^5.3.19",
|
||||
"@storybook/react": "^5.3.19",
|
||||
"@storybook/theming": "^5.3.19",
|
||||
"@testing-library/jest-dom": "^5.8.0",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/react-hooks": "^3.2.1",
|
||||
"@testing-library/jest-dom": "^5.8.0",
|
||||
"@types/angular": "^1.6.56",
|
||||
"@types/archiver": "^3.1.0",
|
||||
"@types/base64-js": "^1.2.5",
|
||||
|
@ -72,8 +72,9 @@
|
|||
"@types/gulp": "^4.0.6",
|
||||
"@types/hapi__wreck": "^15.0.1",
|
||||
"@types/he": "^1.1.1",
|
||||
"@types/hoist-non-react-statics": "^3.3.1",
|
||||
"@types/history": "^4.7.3",
|
||||
"@types/hoist-non-react-statics": "^3.3.1",
|
||||
"@types/http-proxy": "^1.17.4",
|
||||
"@types/jest": "^25.2.3",
|
||||
"@types/jest-specific-snapshot": "^0.5.4",
|
||||
"@types/joi": "^13.4.2",
|
||||
|
@ -94,6 +95,7 @@
|
|||
"@types/object-hash": "^1.3.0",
|
||||
"@types/papaparse": "^5.0.3",
|
||||
"@types/pngjs": "^3.3.2",
|
||||
"@types/pretty-ms": "^5.0.0",
|
||||
"@types/prop-types": "^15.5.3",
|
||||
"@types/proper-lockfile": "^3.0.1",
|
||||
"@types/puppeteer": "^1.20.1",
|
||||
|
@ -109,6 +111,7 @@
|
|||
"@types/redux-actions": "^2.6.1",
|
||||
"@types/set-value": "^2.0.0",
|
||||
"@types/sinon": "^7.0.13",
|
||||
"@types/stats-lite": "^2.2.0",
|
||||
"@types/styled-components": "^5.1.0",
|
||||
"@types/supertest": "^2.0.5",
|
||||
"@types/tar-fs": "^1.16.1",
|
||||
|
@ -116,11 +119,9 @@
|
|||
"@types/tinycolor2": "^1.4.1",
|
||||
"@types/use-resize-observer": "^6.0.0",
|
||||
"@types/uuid": "^3.4.4",
|
||||
"@types/webpack-env": "^1.15.2",
|
||||
"@types/xml-crypto": "^1.4.0",
|
||||
"@types/xml2js": "^0.4.5",
|
||||
"@types/stats-lite": "^2.2.0",
|
||||
"@types/pretty-ms": "^5.0.0",
|
||||
"@types/webpack-env": "^1.15.2",
|
||||
"@welldone-software/why-did-you-render": "^4.0.0",
|
||||
"abab": "^1.0.4",
|
||||
"autoprefixer": "^9.7.4",
|
||||
|
@ -227,6 +228,7 @@
|
|||
"@turf/circle": "6.0.1",
|
||||
"@turf/distance": "6.0.1",
|
||||
"@turf/helpers": "6.0.1",
|
||||
"@types/http-proxy-agent": "^2.0.2",
|
||||
"angular": "^1.8.0",
|
||||
"angular-resource": "1.8.0",
|
||||
"angular-sanitize": "1.8.0",
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
|
||||
import {
|
||||
ExternalIncidentServiceConfigurationSchema,
|
||||
|
@ -122,7 +123,12 @@ export interface ExternalServiceApi {
|
|||
|
||||
export interface CreateExternalServiceBasicArgs {
|
||||
api: ExternalServiceApi;
|
||||
createExternalService: (credentials: ExternalServiceCredentials) => ExternalService;
|
||||
createExternalService: (
|
||||
credentials: ExternalServiceCredentials,
|
||||
logger: Logger,
|
||||
proxySettings?: any
|
||||
) => ExternalService;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export interface CreateExternalServiceArgs extends CreateExternalServiceBasicArgs {
|
||||
|
|
|
@ -67,6 +67,7 @@ export const mapParams = (
|
|||
export const createConnectorExecutor = ({
|
||||
api,
|
||||
createExternalService,
|
||||
logger,
|
||||
}: CreateExternalServiceBasicArgs) => async (
|
||||
execOptions: ActionTypeExecutorOptions<
|
||||
ExternalIncidentServiceConfiguration,
|
||||
|
@ -83,10 +84,14 @@ export const createConnectorExecutor = ({
|
|||
actionId,
|
||||
};
|
||||
|
||||
const externalService = createExternalService({
|
||||
const externalService = createExternalService(
|
||||
{
|
||||
config,
|
||||
secrets,
|
||||
});
|
||||
},
|
||||
logger,
|
||||
execOptions.proxySettings
|
||||
);
|
||||
|
||||
if (!api[subAction]) {
|
||||
throw new Error('[Action][ExternalService] Unsupported subAction type.');
|
||||
|
@ -122,10 +127,11 @@ export const createConnector = ({
|
|||
validate,
|
||||
createExternalService,
|
||||
validationSchema,
|
||||
logger,
|
||||
}: CreateExternalServiceArgs) => {
|
||||
return ({
|
||||
configurationUtilities,
|
||||
executor = createConnectorExecutor({ api, createExternalService }),
|
||||
executor = createConnectorExecutor({ api, createExternalService, logger }),
|
||||
}: CreateActionTypeArgs): ActionType => ({
|
||||
...config,
|
||||
validate: {
|
||||
|
|
|
@ -269,6 +269,7 @@ describe('execute()', () => {
|
|||
"message": "a message to you",
|
||||
"subject": "the subject",
|
||||
},
|
||||
"proxySettings": undefined,
|
||||
"routing": Object {
|
||||
"bcc": Array [
|
||||
"jimmy@example.com",
|
||||
|
@ -326,6 +327,7 @@ describe('execute()', () => {
|
|||
"message": "a message to you",
|
||||
"subject": "the subject",
|
||||
},
|
||||
"proxySettings": undefined,
|
||||
"routing": Object {
|
||||
"bcc": Array [
|
||||
"jimmy@example.com",
|
||||
|
|
|
@ -184,6 +184,7 @@ async function executor(
|
|||
subject: params.subject,
|
||||
message: params.message,
|
||||
},
|
||||
proxySettings: execOptions.proxySettings,
|
||||
};
|
||||
|
||||
let result;
|
||||
|
|
|
@ -31,9 +31,9 @@ export function registerBuiltInActionTypes({
|
|||
actionTypeRegistry.register(getIndexActionType({ logger }));
|
||||
actionTypeRegistry.register(getPagerDutyActionType({ logger, configurationUtilities }));
|
||||
actionTypeRegistry.register(getServerLogActionType({ logger }));
|
||||
actionTypeRegistry.register(getSlackActionType({ configurationUtilities }));
|
||||
actionTypeRegistry.register(getSlackActionType({ logger, configurationUtilities }));
|
||||
actionTypeRegistry.register(getWebhookActionType({ logger, configurationUtilities }));
|
||||
actionTypeRegistry.register(getServiceNowActionType({ logger, configurationUtilities }));
|
||||
actionTypeRegistry.register(getJiraActionType({ configurationUtilities }));
|
||||
actionTypeRegistry.register(getResilientActionType({ configurationUtilities }));
|
||||
actionTypeRegistry.register(getJiraActionType({ logger, configurationUtilities }));
|
||||
actionTypeRegistry.register(getResilientActionType({ logger, configurationUtilities }));
|
||||
}
|
||||
|
|
|
@ -4,15 +4,25 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
import { createConnector } from '../case/utils';
|
||||
import { ActionType } from '../../types';
|
||||
|
||||
import { api } from './api';
|
||||
import { config } from './config';
|
||||
import { validate } from './validators';
|
||||
import { createExternalService } from './service';
|
||||
import { JiraSecretConfiguration, JiraPublicConfiguration } from './schema';
|
||||
import { ActionsConfigurationUtilities } from '../../actions_config';
|
||||
|
||||
export const getActionType = createConnector({
|
||||
export function getActionType({
|
||||
logger,
|
||||
configurationUtilities,
|
||||
}: {
|
||||
logger: Logger;
|
||||
configurationUtilities: ActionsConfigurationUtilities;
|
||||
}): ActionType {
|
||||
return createConnector({
|
||||
api,
|
||||
config,
|
||||
validate,
|
||||
|
@ -21,4 +31,6 @@ export const getActionType = createConnector({
|
|||
config: JiraPublicConfiguration,
|
||||
secrets: JiraSecretConfiguration,
|
||||
},
|
||||
});
|
||||
logger,
|
||||
})({ configurationUtilities });
|
||||
}
|
||||
|
|
|
@ -9,6 +9,9 @@ import axios from 'axios';
|
|||
import { createExternalService } from './service';
|
||||
import * as utils from '../lib/axios_utils';
|
||||
import { ExternalService } from '../case/types';
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
|
||||
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
|
||||
jest.mock('axios');
|
||||
jest.mock('../lib/axios_utils', () => {
|
||||
|
@ -26,10 +29,13 @@ describe('Jira service', () => {
|
|||
let service: ExternalService;
|
||||
|
||||
beforeAll(() => {
|
||||
service = createExternalService({
|
||||
service = createExternalService(
|
||||
{
|
||||
config: { apiUrl: 'https://siem-kibana.atlassian.net', projectKey: 'CK' },
|
||||
secrets: { apiToken: 'token', email: 'elastic@elastic.com' },
|
||||
});
|
||||
},
|
||||
logger
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -39,37 +45,49 @@ describe('Jira service', () => {
|
|||
describe('createExternalService', () => {
|
||||
test('throws without url', () => {
|
||||
expect(() =>
|
||||
createExternalService({
|
||||
createExternalService(
|
||||
{
|
||||
config: { apiUrl: null, projectKey: 'CK' },
|
||||
secrets: { apiToken: 'token', email: 'elastic@elastic.com' },
|
||||
})
|
||||
},
|
||||
logger
|
||||
)
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
test('throws without projectKey', () => {
|
||||
expect(() =>
|
||||
createExternalService({
|
||||
createExternalService(
|
||||
{
|
||||
config: { apiUrl: 'test.com', projectKey: null },
|
||||
secrets: { apiToken: 'token', email: 'elastic@elastic.com' },
|
||||
})
|
||||
},
|
||||
logger
|
||||
)
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
test('throws without username', () => {
|
||||
expect(() =>
|
||||
createExternalService({
|
||||
createExternalService(
|
||||
{
|
||||
config: { apiUrl: 'test.com' },
|
||||
secrets: { apiToken: '', email: 'elastic@elastic.com' },
|
||||
})
|
||||
},
|
||||
logger
|
||||
)
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
test('throws without password', () => {
|
||||
expect(() =>
|
||||
createExternalService({
|
||||
createExternalService(
|
||||
{
|
||||
config: { apiUrl: 'test.com' },
|
||||
secrets: { apiToken: '', email: undefined },
|
||||
})
|
||||
},
|
||||
logger
|
||||
)
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
@ -92,6 +110,7 @@ describe('Jira service', () => {
|
|||
expect(requestMock).toHaveBeenCalledWith({
|
||||
axios,
|
||||
url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1',
|
||||
logger,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -146,6 +165,7 @@ describe('Jira service', () => {
|
|||
expect(requestMock).toHaveBeenCalledWith({
|
||||
axios,
|
||||
url: 'https://siem-kibana.atlassian.net/rest/api/2/issue',
|
||||
logger,
|
||||
method: 'post',
|
||||
data: {
|
||||
fields: {
|
||||
|
@ -210,6 +230,7 @@ describe('Jira service', () => {
|
|||
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
axios,
|
||||
logger,
|
||||
method: 'put',
|
||||
url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1',
|
||||
data: { fields: { summary: 'title', description: 'desc' } },
|
||||
|
@ -272,6 +293,7 @@ describe('Jira service', () => {
|
|||
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
axios,
|
||||
logger,
|
||||
method: 'post',
|
||||
url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1/comment',
|
||||
data: { body: 'comment' },
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import axios from 'axios';
|
||||
|
||||
import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types';
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
import {
|
||||
JiraPublicConfigurationType,
|
||||
JiraSecretConfigurationType,
|
||||
|
@ -17,6 +18,7 @@ import {
|
|||
|
||||
import * as i18n from './translations';
|
||||
import { request, getErrorMessage } from '../lib/axios_utils';
|
||||
import { ProxySettings } from '../../types';
|
||||
|
||||
const VERSION = '2';
|
||||
const BASE_URL = `rest/api/${VERSION}`;
|
||||
|
@ -25,10 +27,11 @@ const COMMENT_URL = `comment`;
|
|||
|
||||
const VIEW_INCIDENT_URL = `browse`;
|
||||
|
||||
export const createExternalService = ({
|
||||
config,
|
||||
secrets,
|
||||
}: ExternalServiceCredentials): ExternalService => {
|
||||
export const createExternalService = (
|
||||
{ config, secrets }: ExternalServiceCredentials,
|
||||
logger: Logger,
|
||||
proxySettings?: ProxySettings
|
||||
): ExternalService => {
|
||||
const { apiUrl: url, projectKey } = config as JiraPublicConfigurationType;
|
||||
const { apiToken, email } = secrets as JiraSecretConfigurationType;
|
||||
|
||||
|
@ -55,6 +58,8 @@ export const createExternalService = ({
|
|||
const res = await request({
|
||||
axios: axiosInstance,
|
||||
url: `${incidentUrl}/${id}`,
|
||||
logger,
|
||||
proxySettings,
|
||||
});
|
||||
|
||||
const { fields, ...rest } = res.data;
|
||||
|
@ -75,10 +80,12 @@ export const createExternalService = ({
|
|||
const res = await request<CreateIncidentRequest>({
|
||||
axios: axiosInstance,
|
||||
url: `${incidentUrl}`,
|
||||
logger,
|
||||
method: 'post',
|
||||
data: {
|
||||
fields: { ...incident, project: { key: projectKey }, issuetype: { name: 'Task' } },
|
||||
},
|
||||
proxySettings,
|
||||
});
|
||||
|
||||
const updatedIncident = await getIncident(res.data.id);
|
||||
|
@ -102,7 +109,9 @@ export const createExternalService = ({
|
|||
axios: axiosInstance,
|
||||
method: 'put',
|
||||
url: `${incidentUrl}/${incidentId}`,
|
||||
logger,
|
||||
data: { fields: { ...incident } },
|
||||
proxySettings,
|
||||
});
|
||||
|
||||
const updatedIncident = await getIncident(incidentId);
|
||||
|
@ -129,7 +138,9 @@ export const createExternalService = ({
|
|||
axios: axiosInstance,
|
||||
method: 'post',
|
||||
url: getCommentsURL(incidentId),
|
||||
logger,
|
||||
data: { body: comment.comment },
|
||||
proxySettings,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -5,7 +5,11 @@
|
|||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import { addTimeZoneToDate, throwIfNotAlive, request, patch, getErrorMessage } from './axios_utils';
|
||||
import HttpProxyAgent from 'http-proxy-agent';
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
import { addTimeZoneToDate, request, patch, getErrorMessage } from './axios_utils';
|
||||
import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
|
||||
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
jest.mock('axios');
|
||||
const axiosMock = (axios as unknown) as jest.Mock;
|
||||
|
||||
|
@ -21,26 +25,6 @@ describe('addTimeZoneToDate', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('throwIfNotAlive ', () => {
|
||||
test('throws correctly when status is invalid', async () => {
|
||||
expect(() => {
|
||||
throwIfNotAlive(404, 'application/json');
|
||||
}).toThrow('Instance is not alive.');
|
||||
});
|
||||
|
||||
test('throws correctly when content is invalid', () => {
|
||||
expect(() => {
|
||||
throwIfNotAlive(200, 'application/html');
|
||||
}).toThrow('Instance is not alive.');
|
||||
});
|
||||
|
||||
test('do NOT throws with custom validStatusCodes', async () => {
|
||||
expect(() => {
|
||||
throwIfNotAlive(404, 'application/json', [404]);
|
||||
}).not.toThrow('Instance is not alive.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('request', () => {
|
||||
beforeEach(() => {
|
||||
axiosMock.mockImplementation(() => ({
|
||||
|
@ -51,9 +35,50 @@ describe('request', () => {
|
|||
});
|
||||
|
||||
test('it fetch correctly with defaults', async () => {
|
||||
const res = await request({ axios, url: '/test' });
|
||||
const res = await request({
|
||||
axios,
|
||||
url: '/test',
|
||||
logger,
|
||||
});
|
||||
|
||||
expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'get', data: {} });
|
||||
expect(axiosMock).toHaveBeenCalledWith('/test', {
|
||||
method: 'get',
|
||||
data: {},
|
||||
headers: undefined,
|
||||
httpAgent: undefined,
|
||||
httpsAgent: undefined,
|
||||
params: undefined,
|
||||
proxy: false,
|
||||
validateStatus: undefined,
|
||||
});
|
||||
expect(res).toEqual({
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
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({
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
|
@ -62,25 +87,24 @@ describe('request', () => {
|
|||
});
|
||||
|
||||
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({
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
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', () => {
|
||||
|
@ -92,8 +116,17 @@ describe('patch', () => {
|
|||
});
|
||||
|
||||
test('it fetch correctly', async () => {
|
||||
await patch({ axios, url: '/test', data: { id: '123' } });
|
||||
expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'patch', data: { id: '123' } });
|
||||
await patch({ axios, url: '/test', data: { id: '123' }, logger });
|
||||
expect(axiosMock).toHaveBeenCalledWith('/test', {
|
||||
method: 'patch',
|
||||
data: { id: '123' },
|
||||
headers: undefined,
|
||||
httpAgent: undefined,
|
||||
httpsAgent: undefined,
|
||||
params: undefined,
|
||||
proxy: false,
|
||||
validateStatus: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -4,50 +4,68 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AxiosInstance, Method, AxiosResponse } from 'axios';
|
||||
|
||||
export const throwIfNotAlive = (
|
||||
status: number,
|
||||
contentType: string,
|
||||
validStatusCodes: number[] = [200, 201, 204]
|
||||
) => {
|
||||
if (!validStatusCodes.includes(status) || !contentType.includes('application/json')) {
|
||||
throw new Error('Instance is not alive.');
|
||||
}
|
||||
};
|
||||
import { AxiosInstance, Method, AxiosResponse, AxiosBasicCredentials } from 'axios';
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
import { ProxySettings } from '../../types';
|
||||
import { getProxyAgent } from './get_proxy_agent';
|
||||
|
||||
export const request = async <T = unknown>({
|
||||
axios,
|
||||
url,
|
||||
logger,
|
||||
method = 'get',
|
||||
data,
|
||||
params,
|
||||
proxySettings,
|
||||
headers,
|
||||
validateStatus,
|
||||
auth,
|
||||
}: {
|
||||
axios: AxiosInstance;
|
||||
url: string;
|
||||
logger: Logger;
|
||||
method?: Method;
|
||||
data?: T;
|
||||
params?: unknown;
|
||||
proxySettings?: ProxySettings;
|
||||
headers?: Record<string, string> | null;
|
||||
validateStatus?: (status: number) => boolean;
|
||||
auth?: AxiosBasicCredentials;
|
||||
}): Promise<AxiosResponse> => {
|
||||
const res = await axios(url, { method, data: data ?? {}, params });
|
||||
throwIfNotAlive(res.status, res.headers['content-type']);
|
||||
return res;
|
||||
return await axios(url, {
|
||||
method,
|
||||
data: data ?? {},
|
||||
params,
|
||||
auth,
|
||||
// use httpsAgent and embedded proxy: false, to be able to handle fail on invalid certs
|
||||
httpsAgent: proxySettings ? getProxyAgent(proxySettings, logger) : undefined,
|
||||
httpAgent: proxySettings ? getProxyAgent(proxySettings, logger) : undefined,
|
||||
proxy: false, // the same way as it done for IncomingWebhook in
|
||||
headers,
|
||||
validateStatus,
|
||||
});
|
||||
};
|
||||
|
||||
export const patch = async <T = unknown>({
|
||||
axios,
|
||||
url,
|
||||
data,
|
||||
logger,
|
||||
proxySettings,
|
||||
}: {
|
||||
axios: AxiosInstance;
|
||||
url: string;
|
||||
data: T;
|
||||
logger: Logger;
|
||||
proxySettings?: ProxySettings;
|
||||
}): Promise<AxiosResponse> => {
|
||||
return request({
|
||||
axios,
|
||||
url,
|
||||
logger,
|
||||
method: 'patch',
|
||||
data,
|
||||
proxySettings,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -5,22 +5,34 @@
|
|||
*/
|
||||
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { Services } from '../../types';
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
import { Services, ProxySettings } from '../../types';
|
||||
import { request } from './axios_utils';
|
||||
|
||||
interface PostPagerdutyOptions {
|
||||
apiUrl: string;
|
||||
data: unknown;
|
||||
headers: Record<string, string>;
|
||||
services: Services;
|
||||
proxySettings?: ProxySettings;
|
||||
}
|
||||
|
||||
// post an event to pagerduty
|
||||
export async function postPagerduty(options: PostPagerdutyOptions): Promise<AxiosResponse> {
|
||||
const { apiUrl, data, headers } = options;
|
||||
const axiosOptions = {
|
||||
export async function postPagerduty(
|
||||
options: PostPagerdutyOptions,
|
||||
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,
|
||||
validateStatus: () => true,
|
||||
};
|
||||
|
||||
return axios.post(apiUrl, data, axiosOptions);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { Logger } from '../../../../../../src/core/server';
|
|||
import { sendEmail } from './send_email';
|
||||
import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
|
||||
import nodemailer from 'nodemailer';
|
||||
import { ProxySettings } from '../../types';
|
||||
|
||||
const createTransportMock = nodemailer.createTransport as jest.Mock;
|
||||
const sendMailMockResult = { result: 'does not matter' };
|
||||
|
@ -63,6 +64,59 @@ describe('send_email module', () => {
|
|||
});
|
||||
|
||||
test('handles unauthenticated email using not secure host/port', async () => {
|
||||
const sendEmailOptions = getSendEmailOptions(
|
||||
{
|
||||
transport: {
|
||||
host: 'example.com',
|
||||
port: 1025,
|
||||
},
|
||||
},
|
||||
{
|
||||
proxyUrl: 'https://example.com',
|
||||
rejectUnauthorizedCertificates: false,
|
||||
}
|
||||
);
|
||||
delete sendEmailOptions.transport.service;
|
||||
delete sendEmailOptions.transport.user;
|
||||
delete sendEmailOptions.transport.password;
|
||||
const result = await sendEmail(mockLogger, sendEmailOptions);
|
||||
expect(result).toBe(sendMailMockResult);
|
||||
expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"headers": undefined,
|
||||
"host": "example.com",
|
||||
"port": 1025,
|
||||
"proxy": "https://example.com",
|
||||
"secure": false,
|
||||
"tls": Object {
|
||||
"rejectUnauthorized": false,
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(sendMailMock.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"bcc": Array [],
|
||||
"cc": Array [
|
||||
"bob@example.com",
|
||||
"robert@example.com",
|
||||
],
|
||||
"from": "fred@example.com",
|
||||
"html": "<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({
|
||||
transport: {
|
||||
host: 'example.com',
|
||||
|
@ -80,9 +134,6 @@ describe('send_email module', () => {
|
|||
"host": "example.com",
|
||||
"port": 1025,
|
||||
"secure": false,
|
||||
"tls": Object {
|
||||
"rejectUnauthorized": false,
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
@ -161,7 +212,10 @@ describe('send_email module', () => {
|
|||
});
|
||||
});
|
||||
|
||||
function getSendEmailOptions({ content = {}, routing = {}, transport = {} } = {}) {
|
||||
function getSendEmailOptions(
|
||||
{ content = {}, routing = {}, transport = {} } = {},
|
||||
proxySettings?: ProxySettings
|
||||
) {
|
||||
return {
|
||||
content: {
|
||||
...content,
|
||||
|
@ -181,5 +235,6 @@ function getSendEmailOptions({ content = {}, routing = {}, transport = {} } = {}
|
|||
user: 'elastic',
|
||||
password: 'changeme',
|
||||
},
|
||||
proxySettings,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
|
||||
// info on nodemailer: https://nodemailer.com/about/
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
import { default as MarkdownIt } from 'markdown-it';
|
||||
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
import { ProxySettings } from '../../types';
|
||||
|
||||
// an email "service" which doesn't actually send, just returns what it would send
|
||||
export const JSON_TRANSPORT_SERVICE = '__json';
|
||||
|
@ -18,6 +18,7 @@ export interface SendEmailOptions {
|
|||
transport: Transport;
|
||||
routing: Routing;
|
||||
content: Content;
|
||||
proxySettings?: ProxySettings;
|
||||
}
|
||||
|
||||
// config validation ensures either service is set or host/port are set
|
||||
|
@ -44,7 +45,7 @@ export interface Content {
|
|||
|
||||
// send an email
|
||||
export async function sendEmail(logger: Logger, options: SendEmailOptions): Promise<unknown> {
|
||||
const { transport, routing, content } = options;
|
||||
const { transport, routing, content, proxySettings } = options;
|
||||
const { service, host, port, secure, user, password } = transport;
|
||||
const { from, to, cc, bcc } = routing;
|
||||
const { subject, message } = content;
|
||||
|
@ -67,11 +68,16 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom
|
|||
transportConfig.host = host;
|
||||
transportConfig.port = port;
|
||||
transportConfig.secure = !!secure;
|
||||
if (!transportConfig.secure) {
|
||||
if (proxySettings && !transportConfig.secure) {
|
||||
transportConfig.tls = {
|
||||
rejectUnauthorized: false,
|
||||
// do not fail on invalid certs if value is false
|
||||
rejectUnauthorized: proxySettings?.rejectUnauthorizedCertificates,
|
||||
};
|
||||
}
|
||||
if (proxySettings) {
|
||||
transportConfig.proxy = proxySettings.proxyUrl;
|
||||
transportConfig.headers = proxySettings.proxyHeaders;
|
||||
}
|
||||
}
|
||||
|
||||
const nodemailerTransport = nodemailer.createTransport(transportConfig);
|
||||
|
|
|
@ -161,6 +161,7 @@ async function executor(
|
|||
const secrets = execOptions.secrets;
|
||||
const params = execOptions.params;
|
||||
const services = execOptions.services;
|
||||
const proxySettings = execOptions.proxySettings;
|
||||
|
||||
const apiUrl = getPagerDutyApiUrl(config);
|
||||
const headers = {
|
||||
|
@ -171,7 +172,7 @@ async function executor(
|
|||
|
||||
let response;
|
||||
try {
|
||||
response = await postPagerduty({ apiUrl, data, headers, services });
|
||||
response = await postPagerduty({ apiUrl, data, headers, services, proxySettings }, logger);
|
||||
} catch (err) {
|
||||
const message = i18n.translate('xpack.actions.builtin.pagerduty.postingErrorMessage', {
|
||||
defaultMessage: 'error posting pagerduty event',
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
import { createConnector } from '../case/utils';
|
||||
|
||||
import { api } from './api';
|
||||
|
@ -11,8 +12,17 @@ import { config } from './config';
|
|||
import { validate } from './validators';
|
||||
import { createExternalService } from './service';
|
||||
import { ResilientSecretConfiguration, ResilientPublicConfiguration } from './schema';
|
||||
import { ActionsConfigurationUtilities } from '../../actions_config';
|
||||
import { ActionType } from '../../types';
|
||||
|
||||
export const getActionType = createConnector({
|
||||
export function getActionType({
|
||||
logger,
|
||||
configurationUtilities,
|
||||
}: {
|
||||
logger: Logger;
|
||||
configurationUtilities: ActionsConfigurationUtilities;
|
||||
}): ActionType {
|
||||
return createConnector({
|
||||
api,
|
||||
config,
|
||||
validate,
|
||||
|
@ -21,4 +31,6 @@ export const getActionType = createConnector({
|
|||
config: ResilientPublicConfiguration,
|
||||
secrets: ResilientSecretConfiguration,
|
||||
},
|
||||
});
|
||||
logger,
|
||||
})({ configurationUtilities });
|
||||
}
|
||||
|
|
|
@ -9,6 +9,9 @@ import axios from 'axios';
|
|||
import { createExternalService, getValueTextContent, formatUpdateRequest } from './service';
|
||||
import * as utils from '../lib/axios_utils';
|
||||
import { ExternalService } from '../case/types';
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
|
||||
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
|
||||
jest.mock('axios');
|
||||
jest.mock('../lib/axios_utils', () => {
|
||||
|
@ -72,10 +75,13 @@ describe('IBM Resilient service', () => {
|
|||
let service: ExternalService;
|
||||
|
||||
beforeAll(() => {
|
||||
service = createExternalService({
|
||||
service = createExternalService(
|
||||
{
|
||||
config: { apiUrl: 'https://resilient.elastic.co', orgId: '201' },
|
||||
secrets: { apiKeyId: 'keyId', apiKeySecret: 'secret' },
|
||||
});
|
||||
},
|
||||
logger
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
|
@ -138,37 +144,49 @@ describe('IBM Resilient service', () => {
|
|||
describe('createExternalService', () => {
|
||||
test('throws without url', () => {
|
||||
expect(() =>
|
||||
createExternalService({
|
||||
createExternalService(
|
||||
{
|
||||
config: { apiUrl: null, orgId: '201' },
|
||||
secrets: { apiKeyId: 'token', apiKeySecret: 'secret' },
|
||||
})
|
||||
},
|
||||
logger
|
||||
)
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
test('throws without orgId', () => {
|
||||
expect(() =>
|
||||
createExternalService({
|
||||
createExternalService(
|
||||
{
|
||||
config: { apiUrl: 'test.com', orgId: null },
|
||||
secrets: { apiKeyId: 'token', apiKeySecret: 'secret' },
|
||||
})
|
||||
},
|
||||
logger
|
||||
)
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
test('throws without username', () => {
|
||||
expect(() =>
|
||||
createExternalService({
|
||||
createExternalService(
|
||||
{
|
||||
config: { apiUrl: 'test.com', orgId: '201' },
|
||||
secrets: { apiKeyId: '', apiKeySecret: 'secret' },
|
||||
})
|
||||
},
|
||||
logger
|
||||
)
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
test('throws without password', () => {
|
||||
expect(() =>
|
||||
createExternalService({
|
||||
createExternalService(
|
||||
{
|
||||
config: { apiUrl: 'test.com', orgId: '201' },
|
||||
secrets: { apiKeyId: '', apiKeySecret: undefined },
|
||||
})
|
||||
},
|
||||
logger
|
||||
)
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
@ -197,6 +215,7 @@ describe('IBM Resilient service', () => {
|
|||
await service.getIncident('1');
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
axios,
|
||||
logger,
|
||||
url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1',
|
||||
params: {
|
||||
text_content_output_format: 'objects_convert',
|
||||
|
@ -256,6 +275,7 @@ describe('IBM Resilient service', () => {
|
|||
expect(requestMock).toHaveBeenCalledWith({
|
||||
axios,
|
||||
url: 'https://resilient.elastic.co/rest/orgs/201/incidents',
|
||||
logger,
|
||||
method: 'post',
|
||||
data: {
|
||||
name: 'title',
|
||||
|
@ -311,6 +331,7 @@ describe('IBM Resilient service', () => {
|
|||
// The second call to the API is the update call.
|
||||
expect(requestMock.mock.calls[1][0]).toEqual({
|
||||
axios,
|
||||
logger,
|
||||
method: 'patch',
|
||||
url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1',
|
||||
data: {
|
||||
|
@ -392,7 +413,9 @@ describe('IBM Resilient service', () => {
|
|||
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
axios,
|
||||
logger,
|
||||
method: 'post',
|
||||
proxySettings: undefined,
|
||||
url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1/comments',
|
||||
data: {
|
||||
text: {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import axios from 'axios';
|
||||
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types';
|
||||
import {
|
||||
ResilientPublicConfigurationType,
|
||||
|
@ -19,6 +20,7 @@ import {
|
|||
|
||||
import * as i18n from './translations';
|
||||
import { getErrorMessage, request } from '../lib/axios_utils';
|
||||
import { ProxySettings } from '../../types';
|
||||
|
||||
const BASE_URL = `rest`;
|
||||
const INCIDENT_URL = `incidents`;
|
||||
|
@ -57,10 +59,11 @@ export const formatUpdateRequest = ({
|
|||
};
|
||||
};
|
||||
|
||||
export const createExternalService = ({
|
||||
config,
|
||||
secrets,
|
||||
}: ExternalServiceCredentials): ExternalService => {
|
||||
export const createExternalService = (
|
||||
{ config, secrets }: ExternalServiceCredentials,
|
||||
logger: Logger,
|
||||
proxySettings?: ProxySettings
|
||||
): ExternalService => {
|
||||
const { apiUrl: url, orgId } = config as ResilientPublicConfigurationType;
|
||||
const { apiKeyId, apiKeySecret } = secrets as ResilientSecretConfigurationType;
|
||||
|
||||
|
@ -88,9 +91,11 @@ export const createExternalService = ({
|
|||
const res = await request({
|
||||
axios: axiosInstance,
|
||||
url: `${incidentUrl}/${id}`,
|
||||
logger,
|
||||
params: {
|
||||
text_content_output_format: 'objects_convert',
|
||||
},
|
||||
proxySettings,
|
||||
});
|
||||
|
||||
return { ...res.data, description: res.data.description?.content ?? '' };
|
||||
|
@ -107,6 +112,7 @@ export const createExternalService = ({
|
|||
axios: axiosInstance,
|
||||
url: `${incidentUrl}`,
|
||||
method: 'post',
|
||||
logger,
|
||||
data: {
|
||||
...incident,
|
||||
description: {
|
||||
|
@ -115,6 +121,7 @@ export const createExternalService = ({
|
|||
},
|
||||
discovered_date: Date.now(),
|
||||
},
|
||||
proxySettings,
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -139,7 +146,9 @@ export const createExternalService = ({
|
|||
axios: axiosInstance,
|
||||
method: 'patch',
|
||||
url: `${incidentUrl}/${incidentId}`,
|
||||
logger,
|
||||
data,
|
||||
proxySettings,
|
||||
});
|
||||
|
||||
if (!res.data.success) {
|
||||
|
@ -170,7 +179,9 @@ export const createExternalService = ({
|
|||
axios: axiosInstance,
|
||||
method: 'post',
|
||||
url: getCommentsURL(incidentId),
|
||||
logger,
|
||||
data: { text: { format: 'text', content: comment.comment } },
|
||||
proxySettings,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -76,10 +76,14 @@ async function executor(
|
|||
const { subAction, subActionParams } = params;
|
||||
let data: PushToServiceResponse | null = null;
|
||||
|
||||
const externalService = createExternalService({
|
||||
const externalService = createExternalService(
|
||||
{
|
||||
config,
|
||||
secrets,
|
||||
});
|
||||
},
|
||||
logger,
|
||||
execOptions.proxySettings
|
||||
);
|
||||
|
||||
if (!api[subAction]) {
|
||||
const errorMessage = `[Action][ExternalService] Unsupported subAction type ${subAction}.`;
|
||||
|
|
|
@ -9,6 +9,9 @@ import axios from 'axios';
|
|||
import { createExternalService } from './service';
|
||||
import * as utils from '../lib/axios_utils';
|
||||
import { ExternalService } from './types';
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
|
||||
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
|
||||
jest.mock('axios');
|
||||
jest.mock('../lib/axios_utils', () => {
|
||||
|
@ -28,10 +31,13 @@ describe('ServiceNow service', () => {
|
|||
let service: ExternalService;
|
||||
|
||||
beforeAll(() => {
|
||||
service = createExternalService({
|
||||
service = createExternalService(
|
||||
{
|
||||
config: { apiUrl: 'https://dev102283.service-now.com' },
|
||||
secrets: { username: 'admin', password: 'admin' },
|
||||
});
|
||||
},
|
||||
logger
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -41,28 +47,37 @@ describe('ServiceNow service', () => {
|
|||
describe('createExternalService', () => {
|
||||
test('throws without url', () => {
|
||||
expect(() =>
|
||||
createExternalService({
|
||||
createExternalService(
|
||||
{
|
||||
config: { apiUrl: null },
|
||||
secrets: { username: 'admin', password: 'admin' },
|
||||
})
|
||||
},
|
||||
logger
|
||||
)
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
test('throws without username', () => {
|
||||
expect(() =>
|
||||
createExternalService({
|
||||
createExternalService(
|
||||
{
|
||||
config: { apiUrl: 'test.com' },
|
||||
secrets: { username: '', password: 'admin' },
|
||||
})
|
||||
},
|
||||
logger
|
||||
)
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
test('throws without password', () => {
|
||||
expect(() =>
|
||||
createExternalService({
|
||||
createExternalService(
|
||||
{
|
||||
config: { apiUrl: 'test.com' },
|
||||
secrets: { username: '', password: undefined },
|
||||
})
|
||||
},
|
||||
logger
|
||||
)
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
@ -84,6 +99,7 @@ describe('ServiceNow service', () => {
|
|||
await service.getIncident('1');
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
axios,
|
||||
logger,
|
||||
url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1',
|
||||
});
|
||||
});
|
||||
|
@ -127,6 +143,7 @@ describe('ServiceNow service', () => {
|
|||
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
axios,
|
||||
logger,
|
||||
url: 'https://dev102283.service-now.com/api/now/v2/table/incident',
|
||||
method: 'post',
|
||||
data: { short_description: 'title', description: 'desc' },
|
||||
|
@ -179,6 +196,7 @@ describe('ServiceNow service', () => {
|
|||
|
||||
expect(patchMock).toHaveBeenCalledWith({
|
||||
axios,
|
||||
logger,
|
||||
url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1',
|
||||
data: { short_description: 'title', description: 'desc' },
|
||||
});
|
||||
|
|
|
@ -9,8 +9,10 @@ import axios from 'axios';
|
|||
import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from './types';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
import { ServiceNowPublicConfigurationType, ServiceNowSecretConfigurationType } from './types';
|
||||
import { request, getErrorMessage, addTimeZoneToDate, patch } from '../lib/axios_utils';
|
||||
import { ProxySettings } from '../../types';
|
||||
|
||||
const API_VERSION = 'v2';
|
||||
const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`;
|
||||
|
@ -18,10 +20,11 @@ const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`;
|
|||
// Based on: https://docs.servicenow.com/bundle/orlando-platform-user-interface/page/use/navigation/reference/r_NavigatingByURLExamples.html
|
||||
const VIEW_INCIDENT_URL = `nav_to.do?uri=incident.do?sys_id=`;
|
||||
|
||||
export const createExternalService = ({
|
||||
config,
|
||||
secrets,
|
||||
}: ExternalServiceCredentials): ExternalService => {
|
||||
export const createExternalService = (
|
||||
{ config, secrets }: ExternalServiceCredentials,
|
||||
logger: Logger,
|
||||
proxySettings?: ProxySettings
|
||||
): ExternalService => {
|
||||
const { apiUrl: url } = config as ServiceNowPublicConfigurationType;
|
||||
const { username, password } = secrets as ServiceNowSecretConfigurationType;
|
||||
|
||||
|
@ -43,6 +46,8 @@ export const createExternalService = ({
|
|||
const res = await request({
|
||||
axios: axiosInstance,
|
||||
url: `${incidentUrl}/${id}`,
|
||||
logger,
|
||||
proxySettings,
|
||||
});
|
||||
|
||||
return { ...res.data.result };
|
||||
|
@ -58,6 +63,8 @@ export const createExternalService = ({
|
|||
const res = await request({
|
||||
axios: axiosInstance,
|
||||
url: incidentUrl,
|
||||
logger,
|
||||
proxySettings,
|
||||
params,
|
||||
});
|
||||
|
||||
|
@ -71,9 +78,13 @@ export const createExternalService = ({
|
|||
|
||||
const createIncident = async ({ incident }: ExternalServiceParams) => {
|
||||
try {
|
||||
logger.warn(`incident error : ${JSON.stringify(proxySettings)}`);
|
||||
logger.warn(`incident error : ${url}`);
|
||||
const res = await request({
|
||||
axios: axiosInstance,
|
||||
url: `${incidentUrl}`,
|
||||
logger,
|
||||
proxySettings,
|
||||
method: 'post',
|
||||
data: { ...(incident as Record<string, unknown>) },
|
||||
});
|
||||
|
@ -96,7 +107,9 @@ export const createExternalService = ({
|
|||
const res = await patch({
|
||||
axios: axiosInstance,
|
||||
url: `${incidentUrl}/${incidentId}`,
|
||||
logger,
|
||||
data: { ...(incident as Record<string, unknown>) },
|
||||
proxySettings,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -4,25 +4,40 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Logger } from '../../../../../src/core/server';
|
||||
import { Services, ActionTypeExecutorResult } from '../types';
|
||||
import { validateParams, validateSecrets } from '../lib';
|
||||
import { getActionType, SlackActionType, SlackActionTypeExecutorOptions } from './slack';
|
||||
import { actionsConfigMock } from '../actions_config.mock';
|
||||
import { actionsMock } from '../mocks';
|
||||
import { createActionTypeRegistry } from './index.test';
|
||||
|
||||
jest.mock('@slack/webhook', () => {
|
||||
return {
|
||||
IncomingWebhook: jest.fn().mockImplementation(() => {
|
||||
return { send: (message: string) => {} };
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const ACTION_TYPE_ID = '.slack';
|
||||
|
||||
const services: Services = actionsMock.createServices();
|
||||
|
||||
let actionType: SlackActionType;
|
||||
let mockedLogger: jest.Mocked<Logger>;
|
||||
|
||||
beforeAll(() => {
|
||||
const { logger } = createActionTypeRegistry();
|
||||
actionType = getActionType({
|
||||
async executor(options) {
|
||||
return { status: 'ok', actionId: options.actionId };
|
||||
},
|
||||
configurationUtilities: actionsConfigMock.create(),
|
||||
logger,
|
||||
});
|
||||
mockedLogger = logger;
|
||||
expect(actionType).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('action registeration', () => {
|
||||
|
@ -83,6 +98,7 @@ describe('validateActionTypeSecrets()', () => {
|
|||
|
||||
test('should validate and pass when the slack webhookUrl is whitelisted', () => {
|
||||
actionType = getActionType({
|
||||
logger: mockedLogger,
|
||||
configurationUtilities: {
|
||||
...actionsConfigMock.create(),
|
||||
ensureWhitelistedUri: (url) => {
|
||||
|
@ -98,9 +114,10 @@ describe('validateActionTypeSecrets()', () => {
|
|||
|
||||
test('config validation returns an error if the specified URL isnt whitelisted', () => {
|
||||
actionType = getActionType({
|
||||
logger: mockedLogger,
|
||||
configurationUtilities: {
|
||||
...actionsConfigMock.create(),
|
||||
ensureWhitelistedHostname: (url) => {
|
||||
ensureWhitelistedHostname: () => {
|
||||
throw new Error(`target hostname is not whitelisted`);
|
||||
},
|
||||
},
|
||||
|
@ -136,6 +153,7 @@ describe('execute()', () => {
|
|||
|
||||
actionType = getActionType({
|
||||
executor: mockSlackExecutor,
|
||||
logger: mockedLogger,
|
||||
configurationUtilities: actionsConfigMock.create(),
|
||||
});
|
||||
});
|
||||
|
@ -147,6 +165,10 @@ describe('execute()', () => {
|
|||
config: {},
|
||||
secrets: { webhookUrl: 'http://example.com' },
|
||||
params: { message: 'this invocation should succeed' },
|
||||
proxySettings: {
|
||||
proxyUrl: 'https://someproxyhost',
|
||||
rejectUnauthorizedCertificates: false,
|
||||
},
|
||||
});
|
||||
expect(response).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -170,4 +192,25 @@ describe('execute()', () => {
|
|||
`"slack mockExecutor failure: this invocation should fail"`
|
||||
);
|
||||
});
|
||||
|
||||
test('calls the mock executor with success proxy', async () => {
|
||||
const actionTypeProxy = getActionType({
|
||||
logger: mockedLogger,
|
||||
configurationUtilities: actionsConfigMock.create(),
|
||||
});
|
||||
await actionTypeProxy.executor({
|
||||
actionId: 'some-id',
|
||||
services,
|
||||
config: {},
|
||||
secrets: { webhookUrl: 'http://example.com' },
|
||||
params: { message: 'this invocation should succeed' },
|
||||
proxySettings: {
|
||||
proxyUrl: 'https://someproxyhost',
|
||||
rejectUnauthorizedCertificates: false,
|
||||
},
|
||||
});
|
||||
expect(mockedLogger.info).toHaveBeenCalledWith(
|
||||
'IncomingWebhook was called with proxyUrl https://someproxyhost'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,11 +6,14 @@
|
|||
|
||||
import { URL } from 'url';
|
||||
import { curry } from 'lodash';
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import HttpProxyAgent from 'http-proxy-agent';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { IncomingWebhook, IncomingWebhookResult } from '@slack/webhook';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { map, getOrElse } from 'fp-ts/lib/Option';
|
||||
import { Logger } from '../../../../../src/core/server';
|
||||
import { getRetryAfterIntervalFromHeaders } from './lib/http_rersponse_retry_header';
|
||||
|
||||
import {
|
||||
|
@ -20,6 +23,7 @@ import {
|
|||
ExecutorType,
|
||||
} from '../types';
|
||||
import { ActionsConfigurationUtilities } from '../actions_config';
|
||||
import { getProxyAgent } from './lib/get_proxy_agent';
|
||||
|
||||
export type SlackActionType = ActionType<{}, ActionTypeSecretsType, ActionParamsType, unknown>;
|
||||
export type SlackActionTypeExecutorOptions = ActionTypeExecutorOptions<
|
||||
|
@ -49,9 +53,11 @@ const ParamsSchema = schema.object({
|
|||
|
||||
// customizing executor is only used for tests
|
||||
export function getActionType({
|
||||
logger,
|
||||
configurationUtilities,
|
||||
executor = slackExecutor,
|
||||
executor = curry(slackExecutor)({ logger }),
|
||||
}: {
|
||||
logger: Logger;
|
||||
configurationUtilities: ActionsConfigurationUtilities;
|
||||
executor?: ExecutorType<{}, ActionTypeSecretsType, ActionParamsType, unknown>;
|
||||
}): SlackActionType {
|
||||
|
@ -99,6 +105,7 @@ function valdiateActionTypeConfig(
|
|||
// action executor
|
||||
|
||||
async function slackExecutor(
|
||||
{ logger }: { logger: Logger },
|
||||
execOptions: SlackActionTypeExecutorOptions
|
||||
): Promise<ActionTypeExecutorResult<unknown>> {
|
||||
const actionId = execOptions.actionId;
|
||||
|
@ -109,10 +116,22 @@ async function slackExecutor(
|
|||
const { webhookUrl } = secrets;
|
||||
const { message } = params;
|
||||
|
||||
let proxyAgent: HttpsProxyAgent | HttpProxyAgent | undefined;
|
||||
if (execOptions.proxySettings) {
|
||||
proxyAgent = getProxyAgent(execOptions.proxySettings, logger);
|
||||
logger.info(`IncomingWebhook was called with proxyUrl ${execOptions.proxySettings.proxyUrl}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const webhook = new IncomingWebhook(webhookUrl);
|
||||
// https://slack.dev/node-slack-sdk/webhook
|
||||
// node-slack-sdk use Axios inside :)
|
||||
const webhook = new IncomingWebhook(webhookUrl, {
|
||||
agent: proxyAgent,
|
||||
});
|
||||
result = await webhook.send(message);
|
||||
} catch (err) {
|
||||
logger.error(`error on ${actionId} slack event: ${err.message}`);
|
||||
|
||||
if (err.original == null || err.original.response == null) {
|
||||
return serviceErrorResult(actionId, err.message);
|
||||
}
|
||||
|
@ -143,6 +162,8 @@ async function slackExecutor(
|
|||
},
|
||||
}
|
||||
);
|
||||
logger.error(`error on ${actionId} slack action: ${errMessage}`);
|
||||
|
||||
return errorResult(actionId, errMessage);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,10 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
jest.mock('axios', () => ({
|
||||
request: jest.fn(),
|
||||
}));
|
||||
|
||||
import { Services } from '../types';
|
||||
import { validateConfig, validateSecrets, validateParams } from '../lib';
|
||||
import { actionsConfigMock } from '../actions_config.mock';
|
||||
|
@ -24,7 +20,22 @@ import {
|
|||
WebhookMethods,
|
||||
} from './webhook';
|
||||
|
||||
const axiosRequestMock = axios.request as jest.Mock;
|
||||
import * as utils from './lib/axios_utils';
|
||||
|
||||
jest.mock('axios');
|
||||
jest.mock('./lib/axios_utils', () => {
|
||||
const originalUtils = jest.requireActual('./lib/axios_utils');
|
||||
return {
|
||||
...originalUtils,
|
||||
request: jest.fn(),
|
||||
patch: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
axios.create = jest.fn(() => axios);
|
||||
const requestMock = utils.request as jest.Mock;
|
||||
|
||||
axios.create = jest.fn(() => axios);
|
||||
|
||||
const ACTION_TYPE_ID = '.webhook';
|
||||
|
||||
|
@ -227,7 +238,7 @@ describe('params validation', () => {
|
|||
|
||||
describe('execute()', () => {
|
||||
beforeAll(() => {
|
||||
axiosRequestMock.mockReset();
|
||||
requestMock.mockReset();
|
||||
actionType = getActionType({
|
||||
logger: mockedLogger,
|
||||
configurationUtilities: actionsConfigMock.create(),
|
||||
|
@ -235,8 +246,8 @@ describe('execute()', () => {
|
|||
});
|
||||
|
||||
beforeEach(() => {
|
||||
axiosRequestMock.mockReset();
|
||||
axiosRequestMock.mockResolvedValue({
|
||||
requestMock.mockReset();
|
||||
requestMock.mockResolvedValue({
|
||||
status: 200,
|
||||
statusText: '',
|
||||
data: '',
|
||||
|
@ -261,17 +272,42 @@ describe('execute()', () => {
|
|||
params: { body: 'some data' },
|
||||
});
|
||||
|
||||
expect(axiosRequestMock.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"auth": Object {
|
||||
"password": "123",
|
||||
"username": "abc",
|
||||
},
|
||||
"axios": undefined,
|
||||
"data": "some data",
|
||||
"headers": Object {
|
||||
"aheader": "a value",
|
||||
},
|
||||
"logger": Object {
|
||||
"context": Array [],
|
||||
"debug": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
"response from webhook action \\"some-id\\": [HTTP 200] ",
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
"error": [MockFunction],
|
||||
"fatal": [MockFunction],
|
||||
"get": [MockFunction],
|
||||
"info": [MockFunction],
|
||||
"log": [MockFunction],
|
||||
"trace": [MockFunction],
|
||||
"warn": [MockFunction],
|
||||
},
|
||||
"method": "post",
|
||||
"proxySettings": undefined,
|
||||
"url": "https://abc.def/my-webhook",
|
||||
}
|
||||
`);
|
||||
|
@ -294,13 +330,38 @@ describe('execute()', () => {
|
|||
params: { body: 'some data' },
|
||||
});
|
||||
|
||||
expect(axiosRequestMock.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"axios": undefined,
|
||||
"data": "some data",
|
||||
"headers": Object {
|
||||
"aheader": "a value",
|
||||
},
|
||||
"logger": Object {
|
||||
"context": Array [],
|
||||
"debug": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
"response from webhook action \\"some-id\\": [HTTP 200] ",
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
"error": [MockFunction],
|
||||
"fatal": [MockFunction],
|
||||
"get": [MockFunction],
|
||||
"info": [MockFunction],
|
||||
"log": [MockFunction],
|
||||
"trace": [MockFunction],
|
||||
"warn": [MockFunction],
|
||||
},
|
||||
"method": "post",
|
||||
"proxySettings": undefined,
|
||||
"url": "https://abc.def/my-webhook",
|
||||
}
|
||||
`);
|
||||
|
|
|
@ -15,6 +15,7 @@ import { isOk, promiseResult, Result } from './lib/result_type';
|
|||
import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types';
|
||||
import { ActionsConfigurationUtilities } from '../actions_config';
|
||||
import { Logger } from '../../../../../src/core/server';
|
||||
import { request } from './lib/axios_utils';
|
||||
|
||||
// config definition
|
||||
export enum WebhookMethods {
|
||||
|
@ -136,13 +137,18 @@ export async function executor(
|
|||
? { auth: { username: secrets.user, password: secrets.password } }
|
||||
: {};
|
||||
|
||||
const axiosInstance = axios.create();
|
||||
|
||||
const result: Result<AxiosResponse, AxiosError> = await promiseResult(
|
||||
axios.request({
|
||||
request({
|
||||
axios: axiosInstance,
|
||||
method,
|
||||
url,
|
||||
logger,
|
||||
...basicAuth,
|
||||
headers,
|
||||
data,
|
||||
proxySettings: execOptions.proxySettings,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -159,7 +165,7 @@ export async function executor(
|
|||
if (error.response) {
|
||||
const { status, statusText, headers: responseHeaders } = error.response;
|
||||
const message = `[${status}] ${statusText}`;
|
||||
logger.warn(`error on ${actionId} webhook event: ${message}`);
|
||||
logger.error(`error on ${actionId} webhook event: ${message}`);
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
// special handling for 5xx
|
||||
|
@ -178,7 +184,7 @@ export async function executor(
|
|||
return errorResultInvalid(actionId, message);
|
||||
}
|
||||
|
||||
logger.warn(`error on ${actionId} webhook action: unexpected error`);
|
||||
logger.error(`error on ${actionId} webhook action: unexpected error`);
|
||||
return errorResultUnexpectedError(actionId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ describe('config validation', () => {
|
|||
"*",
|
||||
],
|
||||
"preconfigured": Object {},
|
||||
"rejectUnauthorizedCertificates": true,
|
||||
"whitelistedHosts": Array [
|
||||
"*",
|
||||
],
|
||||
|
@ -33,6 +34,7 @@ describe('config validation', () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
rejectUnauthorizedCertificates: false,
|
||||
};
|
||||
expect(configSchema.validate(config)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -50,6 +52,7 @@ describe('config validation', () => {
|
|||
"secrets": Object {},
|
||||
},
|
||||
},
|
||||
"rejectUnauthorizedCertificates": false,
|
||||
"whitelistedHosts": Array [
|
||||
"*",
|
||||
],
|
||||
|
|
|
@ -32,6 +32,9 @@ export const configSchema = schema.object({
|
|||
defaultValue: {},
|
||||
validate: validatePreconfigured,
|
||||
}),
|
||||
proxyUrl: schema.maybe(schema.string()),
|
||||
proxyHeaders: schema.maybe(schema.recordOf(schema.string(), schema.string())),
|
||||
rejectUnauthorizedCertificates: schema.boolean({ defaultValue: true }),
|
||||
});
|
||||
|
||||
export type ActionsConfig = TypeOf<typeof configSchema>;
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
GetServicesFunction,
|
||||
RawAction,
|
||||
PreConfiguredAction,
|
||||
ProxySettings,
|
||||
} from '../types';
|
||||
import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server';
|
||||
import { SpacesServiceSetup } from '../../../spaces/server';
|
||||
|
@ -28,6 +29,7 @@ export interface ActionExecutorContext {
|
|||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
eventLogger: IEventLogger;
|
||||
preconfiguredActions: PreConfiguredAction[];
|
||||
proxySettings?: ProxySettings;
|
||||
}
|
||||
|
||||
export interface ExecuteOptions {
|
||||
|
@ -78,6 +80,7 @@ export class ActionExecutor {
|
|||
eventLogger,
|
||||
preconfiguredActions,
|
||||
getActionsClientWithRequest,
|
||||
proxySettings,
|
||||
} = this.actionExecutorContext!;
|
||||
|
||||
const services = getServices(request);
|
||||
|
@ -133,6 +136,7 @@ export class ActionExecutor {
|
|||
params: validatedParams,
|
||||
config: validatedConfig,
|
||||
secrets: validatedSecrets,
|
||||
proxySettings,
|
||||
});
|
||||
} catch (err) {
|
||||
rawResult = {
|
||||
|
|
|
@ -34,6 +34,7 @@ describe('Actions Plugin', () => {
|
|||
enabledActionTypes: ['*'],
|
||||
whitelistedHosts: ['*'],
|
||||
preconfigured: {},
|
||||
rejectUnauthorizedCertificates: true,
|
||||
});
|
||||
plugin = new ActionsPlugin(context);
|
||||
coreSetup = coreMock.createSetup();
|
||||
|
@ -194,6 +195,7 @@ describe('Actions Plugin', () => {
|
|||
secrets: {},
|
||||
},
|
||||
},
|
||||
rejectUnauthorizedCertificates: true,
|
||||
});
|
||||
plugin = new ActionsPlugin(context);
|
||||
coreSetup = coreMock.createSetup();
|
||||
|
@ -217,7 +219,7 @@ describe('Actions Plugin', () => {
|
|||
// coreMock.createSetup doesn't support Plugin generics
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await plugin.setup(coreSetup as any, pluginsSetup);
|
||||
const pluginStart = plugin.start(coreStart, pluginsStart);
|
||||
const pluginStart = await plugin.start(coreStart, pluginsStart);
|
||||
|
||||
expect(pluginStart.isActionExecutable('preconfiguredServerLog', '.server-log')).toBe(true);
|
||||
});
|
||||
|
@ -232,7 +234,7 @@ describe('Actions Plugin', () => {
|
|||
usingEphemeralEncryptionKey: false,
|
||||
},
|
||||
});
|
||||
const pluginStart = plugin.start(coreStart, pluginsStart);
|
||||
const pluginStart = await plugin.start(coreStart, pluginsStart);
|
||||
|
||||
await pluginStart.getActionsClientWithRequest(httpServerMock.createKibanaRequest());
|
||||
});
|
||||
|
@ -241,7 +243,7 @@ describe('Actions Plugin', () => {
|
|||
// coreMock.createSetup doesn't support Plugin generics
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await plugin.setup(coreSetup as any, pluginsSetup);
|
||||
const pluginStart = plugin.start(coreStart, pluginsStart);
|
||||
const pluginStart = await plugin.start(coreStart, pluginsStart);
|
||||
|
||||
expect(pluginsSetup.encryptedSavedObjects.usingEphemeralEncryptionKey).toEqual(true);
|
||||
await expect(
|
||||
|
|
|
@ -116,6 +116,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
|
|||
private readonly config: Promise<ActionsConfig>;
|
||||
|
||||
private readonly logger: Logger;
|
||||
private actionsConfig?: ActionsConfig;
|
||||
private serverBasePath?: string;
|
||||
private taskRunnerFactory?: TaskRunnerFactory;
|
||||
private actionTypeRegistry?: ActionTypeRegistry;
|
||||
|
@ -173,12 +174,12 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
|
|||
|
||||
// get executions count
|
||||
const taskRunnerFactory = new TaskRunnerFactory(actionExecutor);
|
||||
const actionsConfig = (await this.config) as ActionsConfig;
|
||||
const actionsConfigUtils = getActionsConfigurationUtilities(actionsConfig);
|
||||
this.actionsConfig = (await this.config) as ActionsConfig;
|
||||
const actionsConfigUtils = getActionsConfigurationUtilities(this.actionsConfig);
|
||||
|
||||
for (const preconfiguredId of Object.keys(actionsConfig.preconfigured)) {
|
||||
for (const preconfiguredId of Object.keys(this.actionsConfig.preconfigured)) {
|
||||
this.preconfiguredActions.push({
|
||||
...actionsConfig.preconfigured[preconfiguredId],
|
||||
...this.actionsConfig.preconfigured[preconfiguredId],
|
||||
id: preconfiguredId,
|
||||
isPreconfigured: true,
|
||||
});
|
||||
|
@ -317,6 +318,14 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
|
|||
encryptedSavedObjectsClient,
|
||||
actionTypeRegistry: actionTypeRegistry!,
|
||||
preconfiguredActions,
|
||||
proxySettings:
|
||||
this.actionsConfig && this.actionsConfig.proxyUrl
|
||||
? {
|
||||
proxyUrl: this.actionsConfig.proxyUrl,
|
||||
proxyHeaders: this.actionsConfig.proxyHeaders,
|
||||
rejectUnauthorizedCertificates: this.actionsConfig.rejectUnauthorizedCertificates,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
taskRunnerFactory!.initialize({
|
||||
|
|
|
@ -58,6 +58,7 @@ export interface ActionTypeExecutorOptions<Config, Secrets, Params> {
|
|||
config: Config;
|
||||
secrets: Secrets;
|
||||
params: Params;
|
||||
proxySettings?: ProxySettings;
|
||||
}
|
||||
|
||||
export interface ActionResult<Config extends ActionTypeConfig = ActionTypeConfig> {
|
||||
|
@ -140,3 +141,9 @@ export interface ActionTaskExecutorParams {
|
|||
spaceId: string;
|
||||
actionTaskParamsId: string;
|
||||
}
|
||||
|
||||
export interface ProxySettings {
|
||||
proxyUrl: string;
|
||||
proxyHeaders?: Record<string, string>;
|
||||
rejectUnauthorizedCertificates: boolean;
|
||||
}
|
||||
|
|
|
@ -11,4 +11,5 @@ export default createTestConfig('basic', {
|
|||
disabledPlugins: [],
|
||||
license: 'basic',
|
||||
ssl: true,
|
||||
enableActionsProxy: false,
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import path from 'path';
|
||||
import getPort from 'get-port';
|
||||
import fs from 'fs';
|
||||
import { CA_CERT_PATH } from '@kbn/dev-utils';
|
||||
import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
|
||||
|
@ -15,6 +16,7 @@ interface CreateTestConfigOptions {
|
|||
license: string;
|
||||
disabledPlugins?: string[];
|
||||
ssl?: boolean;
|
||||
enableActionsProxy: boolean;
|
||||
}
|
||||
|
||||
// test.not-enabled is specifically not enabled
|
||||
|
@ -56,6 +58,10 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
|
|||
fs.statSync(path.resolve(__dirname, 'fixtures', 'plugins', file)).isDirectory()
|
||||
);
|
||||
|
||||
const actionsProxyUrl = options.enableActionsProxy
|
||||
? [`--xpack.actions.proxyUrl=http://localhost:${await getPort()}`]
|
||||
: [];
|
||||
|
||||
return {
|
||||
testFiles: [require.resolve(`../${name}/tests/`)],
|
||||
servers,
|
||||
|
@ -85,6 +91,9 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
|
|||
])}`,
|
||||
'--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"',
|
||||
`--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`,
|
||||
...actionsProxyUrl,
|
||||
'--xpack.actions.rejectUnauthorizedCertificates=false',
|
||||
|
||||
'--xpack.eventLog.logEntries=true',
|
||||
`--xpack.actions.preconfigured=${JSON.stringify({
|
||||
'my-slack1': {
|
||||
|
|
|
@ -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);
|
||||
};
|
|
@ -11,4 +11,5 @@ export default createTestConfig('security_and_spaces', {
|
|||
disabledPlugins: [],
|
||||
license: 'trial',
|
||||
ssl: true,
|
||||
enableActionsProxy: true,
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
import {
|
||||
|
@ -35,6 +36,7 @@ const mapping = [
|
|||
export default function jiraTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const config = getService('config');
|
||||
|
||||
const mockJira = {
|
||||
config: {
|
||||
|
@ -73,12 +75,19 @@ export default function jiraTest({ getService }: FtrProviderContext) {
|
|||
};
|
||||
|
||||
let jiraSimulatorURL: string = '<could not determine kibana url>';
|
||||
let proxyServer: any;
|
||||
let proxyHaveBeenCalled = false;
|
||||
|
||||
describe('Jira', () => {
|
||||
before(() => {
|
||||
jiraSimulatorURL = kibanaServer.resolveUrl(
|
||||
getExternalServiceSimulatorPath(ExternalServiceSimulator.JIRA)
|
||||
);
|
||||
proxyServer = getHttpProxyServer(kibanaServer.resolveUrl('/'), () => {
|
||||
proxyHaveBeenCalled = true;
|
||||
});
|
||||
const proxyUrl = getProxyUrl(config.get('kbnTestServer.serverArgs'));
|
||||
proxyServer.listen(Number(proxyUrl.port));
|
||||
});
|
||||
|
||||
describe('Jira - Action Creation', () => {
|
||||
|
@ -529,6 +538,8 @@ export default function jiraTest({ getService }: FtrProviderContext) {
|
|||
})
|
||||
.expect(200);
|
||||
|
||||
expect(proxyHaveBeenCalled).to.equal(true);
|
||||
|
||||
expect(body).to.eql({
|
||||
status: 'ok',
|
||||
actionId: simulatedActionId,
|
||||
|
@ -542,5 +553,9 @@ export default function jiraTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
proxyServer.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
import {
|
||||
|
@ -17,16 +18,25 @@ import {
|
|||
export default function pagerdutyTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const config = getService('config');
|
||||
|
||||
describe('pagerduty action', () => {
|
||||
let simulatedActionId = '';
|
||||
let pagerdutySimulatorURL: string = '<could not determine kibana url>';
|
||||
let proxyServer: any;
|
||||
let proxyHaveBeenCalled = false;
|
||||
|
||||
// need to wait for kibanaServer to settle ...
|
||||
before(() => {
|
||||
pagerdutySimulatorURL = kibanaServer.resolveUrl(
|
||||
getExternalServiceSimulatorPath(ExternalServiceSimulator.PAGERDUTY)
|
||||
);
|
||||
|
||||
proxyServer = getHttpProxyServer(kibanaServer.resolveUrl('/'), () => {
|
||||
proxyHaveBeenCalled = true;
|
||||
});
|
||||
const proxyUrl = getProxyUrl(config.get('kbnTestServer.serverArgs'));
|
||||
proxyServer.listen(Number(proxyUrl.port));
|
||||
});
|
||||
|
||||
it('should return successfully when passed valid create parameters', async () => {
|
||||
|
@ -144,6 +154,8 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) {
|
|||
},
|
||||
})
|
||||
.expect(200);
|
||||
expect(proxyHaveBeenCalled).to.equal(true);
|
||||
|
||||
expect(result).to.eql({
|
||||
status: 'ok',
|
||||
actionId: simulatedActionId,
|
||||
|
@ -202,5 +214,9 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) {
|
|||
expect(result.message).to.match(/error posting pagerduty event: http status 502/);
|
||||
expect(result.retry).to.equal(true);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
proxyServer.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
import {
|
||||
|
@ -35,6 +36,7 @@ const mapping = [
|
|||
export default function resilientTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const config = getService('config');
|
||||
|
||||
const mockResilient = {
|
||||
config: {
|
||||
|
@ -73,12 +75,19 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
};
|
||||
|
||||
let resilientSimulatorURL: string = '<could not determine kibana url>';
|
||||
let proxyServer: any;
|
||||
let proxyHaveBeenCalled = false;
|
||||
|
||||
describe('IBM Resilient', () => {
|
||||
before(() => {
|
||||
resilientSimulatorURL = kibanaServer.resolveUrl(
|
||||
getExternalServiceSimulatorPath(ExternalServiceSimulator.RESILIENT)
|
||||
);
|
||||
proxyServer = getHttpProxyServer(kibanaServer.resolveUrl('/'), () => {
|
||||
proxyHaveBeenCalled = true;
|
||||
});
|
||||
const proxyUrl = getProxyUrl(config.get('kbnTestServer.serverArgs'));
|
||||
proxyServer.listen(Number(proxyUrl.port));
|
||||
});
|
||||
|
||||
describe('IBM Resilient - Action Creation', () => {
|
||||
|
@ -529,6 +538,8 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
})
|
||||
.expect(200);
|
||||
|
||||
expect(proxyHaveBeenCalled).to.equal(true);
|
||||
|
||||
expect(body).to.eql({
|
||||
status: 'ok',
|
||||
actionId: simulatedActionId,
|
||||
|
@ -542,5 +553,9 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
proxyServer.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
import {
|
||||
|
@ -35,6 +36,7 @@ const mapping = [
|
|||
export default function servicenowTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const config = getService('config');
|
||||
|
||||
const mockServiceNow = {
|
||||
config: {
|
||||
|
@ -72,12 +74,20 @@ export default function servicenowTest({ getService }: FtrProviderContext) {
|
|||
};
|
||||
|
||||
let servicenowSimulatorURL: string = '<could not determine kibana url>';
|
||||
let proxyServer: any;
|
||||
let proxyHaveBeenCalled = false;
|
||||
|
||||
describe('ServiceNow', () => {
|
||||
before(() => {
|
||||
servicenowSimulatorURL = kibanaServer.resolveUrl(
|
||||
getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW)
|
||||
);
|
||||
|
||||
proxyServer = getHttpProxyServer(kibanaServer.resolveUrl('/'), () => {
|
||||
proxyHaveBeenCalled = true;
|
||||
});
|
||||
const proxyUrl = getProxyUrl(config.get('kbnTestServer.serverArgs'));
|
||||
proxyServer.listen(Number(proxyUrl.port));
|
||||
});
|
||||
|
||||
describe('ServiceNow - Action Creation', () => {
|
||||
|
@ -448,6 +458,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) {
|
|||
},
|
||||
})
|
||||
.expect(200);
|
||||
expect(proxyHaveBeenCalled).to.equal(true);
|
||||
|
||||
expect(result).to.eql({
|
||||
status: 'ok',
|
||||
|
@ -462,5 +473,9 @@ export default function servicenowTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
proxyServer.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import expect from '@kbn/expect';
|
||||
import http from 'http';
|
||||
import getPort from 'get-port';
|
||||
import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
import { getSlackServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin';
|
||||
|
@ -14,18 +15,27 @@ import { getSlackServer } from '../../../../common/fixtures/plugins/actions_simu
|
|||
// eslint-disable-next-line import/no-default-export
|
||||
export default function slackTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const config = getService('config');
|
||||
|
||||
describe('slack action', () => {
|
||||
let simulatedActionId = '';
|
||||
|
||||
let slackSimulatorURL: string = '';
|
||||
let slackServer: http.Server;
|
||||
let proxyServer: any;
|
||||
let proxyHaveBeenCalled = false;
|
||||
// need to wait for kibanaServer to settle ...
|
||||
before(async () => {
|
||||
slackServer = await getSlackServer();
|
||||
const availablePort = await getPort({ port: 9000 });
|
||||
slackServer.listen(availablePort);
|
||||
slackSimulatorURL = `http://localhost:${availablePort}`;
|
||||
|
||||
proxyServer = getHttpProxyServer(slackSimulatorURL, () => {
|
||||
proxyHaveBeenCalled = true;
|
||||
});
|
||||
const proxyUrl = getProxyUrl(config.get('kbnTestServer.serverArgs'));
|
||||
proxyServer.listen(Number(proxyUrl.port));
|
||||
});
|
||||
|
||||
it('should return 200 when creating a slack action successfully', async () => {
|
||||
|
@ -155,6 +165,7 @@ export default function slackTest({ getService }: FtrProviderContext) {
|
|||
})
|
||||
.expect(200);
|
||||
expect(result.status).to.eql('ok');
|
||||
expect(proxyHaveBeenCalled).to.equal(true);
|
||||
});
|
||||
|
||||
it('should handle an empty message error', async () => {
|
||||
|
@ -222,6 +233,7 @@ export default function slackTest({ getService }: FtrProviderContext) {
|
|||
|
||||
after(() => {
|
||||
slackServer.close();
|
||||
proxyServer.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import http from 'http';
|
|||
import getPort from 'get-port';
|
||||
import expect from '@kbn/expect';
|
||||
import { URL, format as formatUrl } from 'url';
|
||||
import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
import {
|
||||
getExternalServiceSimulatorPath,
|
||||
|
@ -31,6 +32,7 @@ function parsePort(url: Record<string, string>): Record<string, string | null |
|
|||
export default function webhookTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const configService = getService('config');
|
||||
|
||||
async function createWebhookAction(
|
||||
webhookSimulatorURL: string,
|
||||
|
@ -69,6 +71,8 @@ export default function webhookTest({ getService }: FtrProviderContext) {
|
|||
let webhookSimulatorURL: string = '';
|
||||
let webhookServer: http.Server;
|
||||
let kibanaURL: string = '<could not determine kibana url>';
|
||||
let proxyServer: any;
|
||||
let proxyHaveBeenCalled = false;
|
||||
// need to wait for kibanaServer to settle ...
|
||||
before(async () => {
|
||||
webhookServer = await getWebhookServer();
|
||||
|
@ -76,6 +80,12 @@ export default function webhookTest({ getService }: FtrProviderContext) {
|
|||
webhookServer.listen(availablePort);
|
||||
webhookSimulatorURL = `http://localhost:${availablePort}`;
|
||||
|
||||
proxyServer = getHttpProxyServer(webhookSimulatorURL, () => {
|
||||
proxyHaveBeenCalled = true;
|
||||
});
|
||||
const proxyUrl = getProxyUrl(configService.get('kbnTestServer.serverArgs'));
|
||||
proxyServer.listen(Number(proxyUrl.port));
|
||||
|
||||
kibanaURL = kibanaServer.resolveUrl(
|
||||
getExternalServiceSimulatorPath(ExternalServiceSimulator.WEBHOOK)
|
||||
);
|
||||
|
@ -140,6 +150,7 @@ export default function webhookTest({ getService }: FtrProviderContext) {
|
|||
.expect(200);
|
||||
|
||||
expect(result.status).to.eql('ok');
|
||||
expect(proxyHaveBeenCalled).to.equal(true);
|
||||
});
|
||||
|
||||
it('should support the POST method against webhook target', async () => {
|
||||
|
@ -218,7 +229,7 @@ export default function webhookTest({ getService }: FtrProviderContext) {
|
|||
.expect(200);
|
||||
|
||||
expect(result.status).to.eql('error');
|
||||
expect(result.message).to.match(/error calling webhook, unexpected error/);
|
||||
expect(result.message).to.match(/error calling webhook, retry later/);
|
||||
});
|
||||
|
||||
it('should handle failing webhook targets', async () => {
|
||||
|
@ -240,6 +251,7 @@ export default function webhookTest({ getService }: FtrProviderContext) {
|
|||
|
||||
after(() => {
|
||||
webhookServer.close();
|
||||
proxyServer.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -7,4 +7,8 @@
|
|||
import { createTestConfig } from '../common/config';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default createTestConfig('spaces_only', { disabledPlugins: ['security'], license: 'trial' });
|
||||
export default createTestConfig('spaces_only', {
|
||||
disabledPlugins: ['security'],
|
||||
license: 'trial',
|
||||
enableActionsProxy: false,
|
||||
});
|
||||
|
|
14
yarn.lock
14
yarn.lock
|
@ -3913,6 +3913,20 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz#9140779736aa2655635ee756e2467d787cfe8a2a"
|
||||
integrity sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A==
|
||||
|
||||
"@types/http-proxy-agent@^2.0.2":
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/http-proxy-agent/-/http-proxy-agent-2.0.2.tgz#942c1f35c7e1f0edd1b6ffae5d0f9051cfb32be1"
|
||||
integrity sha512-2S6IuBRhqUnH1/AUx9k8KWtY3Esg4eqri946MnxTG5HwehF1S5mqLln8fcyMiuQkY72p2gH3W+rIPqp5li0LyQ==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/http-proxy@^1.17.4":
|
||||
version "1.17.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.4.tgz#e7c92e3dbe3e13aa799440ff42e6d3a17a9d045b"
|
||||
integrity sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/inert@^5.1.2":
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/inert/-/inert-5.1.2.tgz#2bb8bef3b2462f904c960654c9edfa39285a85c6"
|
||||
|
|
Loading…
Reference in a new issue