diff --git a/.eslintignore b/.eslintignore index b947077..4cbc5a5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ node_modules/ dist/ +__test__/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3dd046a..587cc5a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,8 +6,8 @@ branches: - main paths-ignore: - - '**.md' - - '.github/workflows/codeql.yml' + - '**.md' + - '.github/workflows/codeql.yml' push: paths-ignore: - '**.md' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 44a0811..7bc6073 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -15,12 +15,12 @@ on: push: branches: [ "main" ] paths-ignore: - - '**.md' + - '**.md' pull_request: # The branches below must be a subset of the branches above branches: [ "main" ] paths-ignore: - - '**.md' + - '**.md' schedule: - cron: '32 20 * * 4' diff --git a/README.md b/README.md index bbf727e..e3e9926 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,17 @@ The contents of this repository are individually maintained and are not a direct - `target-url`: **String**, **Required**, The target URL destination where webhook event payloads will be reflected to. - `webhook-secret`: **String**, **Optional**, Secret data value to use for webhook payload. Populates `X-Hub-Signature` and `X-Hub-Signature-256` header values. See [Securing Your Webhooks](https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks) for additional context. +- `allow-list-source`: **String**, **Optional**, Source location for a newline-delimited list of entries which specify the allow-list for `target-url` filtering. Example `allow-list` file contents: + + ```plain + # example-allow-list.txt + # Comment line and blank lines are ignored + + # Entries in this file should be prefixed with transport type (http/https) + https://github.com + https://api.github.com + https://*.github.localdomain + ``` ### Example @@ -55,6 +66,6 @@ Example overview of an implementation which uses reverse proxy or an API gateway - [ ] Complete documentation - [X] Add logic for handling secrets to downstream webhook targets -- [ ] Add code testing +- [X] Add code testing - [ ] Add quick setup example references for easy testing - [ ] Add URL filtering capability to handle controlling valid URL targets via configuration (allow-list). diff --git a/__test__/allowlist.mock b/__test__/allowlist.mock new file mode 100644 index 0000000..1758c32 --- /dev/null +++ b/__test__/allowlist.mock @@ -0,0 +1,9 @@ +# This is a comment line which is ignored +# Blank lines are also ignored + +# Lines in this file should be prefixed with transport type (http/https) + +https://github.com +https://api.github.com +https://*.github.localdomain +https://smee.io diff --git a/__test__/reflector.test.js b/__test__/reflector.test.js new file mode 100644 index 0000000..939901d --- /dev/null +++ b/__test__/reflector.test.js @@ -0,0 +1,88 @@ +// Tests for functions in reflector.js + +const reflector = require('../src/reflector'); +const { validateUrl, fetchAllowListSource, validateAllowList, getWebhookSignature, getRequestOptions } = reflector.reflectorPrivate; + +let allowListObject = ['https://github.com', 'https://api.github.com', 'https://*.github.localdomain', 'https://smee.io']; + +describe('validateUrl', () => { + test('validateUrl() returns true for valid URL', () => { + expect(validateUrl('https://github.com')).toBe(true); + }); + + test('validateUrl() throws error for invalid URL', () => { + expect(() => { + validateUrl('github.com'); + }).toThrow(); + }); +}); + +describe('fetchAllowListSource', () => { + let allowListSource = './__test__/allowlist.mock'; + + test('fetchAllowListSource() returns an object and that members contain mock values', async () => { + let allowList = await fetchAllowListSource(allowListSource); + + expect(typeof allowList).toBe('object'); + + expect(allowList.sort()).toEqual(allowListObject.sort()); + }); +}); + +describe('validateAllowList', () => { + let allowList = allowListObject; + + test('validateAllowList() returns true for valid URL', () => { + expect(validateAllowList('https://api.github.com', allowList)).toBe(true); + }); + + test('validateAllowList() returns true for valid URL with wildcard', () => { + expect(validateAllowList('https://api.github.localdomain', allowList)).toBe(true); + }); + + test('validateAllowList() returns false for invalid URL', () => { + expect(validateAllowList('https://invalid.url', allowList)).toBe(false); + }); +}); + +describe('getWebhookSignature', () => { + + test('getWebhookSignature() returns a sha1 string with the proper value', () => { + let payload = 'payload'; + let secret = 'abc123'; + let algorithm = 'sha1'; + let expected = 'sha1=bf995fbe34d0a428d0cf1d7d45c8990ccefc9250'; + expect(getWebhookSignature(payload, secret, algorithm)).toBe(expected); + }); + + test('getWebhookSignature() returns a sha256 string with the proper value', () => { + let payload = 'payload'; + let secret = 'abc123'; + let algorithm = 'sha256'; + let expected = 'sha256=ba245390d5b4bf305fbef57917c6919d580db46f6989347b7a1f03c4fced02c1'; + expect(getWebhookSignature(payload, secret, algorithm)).toBe(expected); + }); +}); + +describe('getRequestOptions', () => { + let context = { + eventName: 'push', + payload: { + test: 'test' + } + }; + let targetUrl = 'https://github.com'; + let webhookSecret = 'abc123'; + + test('getRequestOptions() returns an object with the proper values', () => { + let options = getRequestOptions(context, targetUrl, webhookSecret); + expect(typeof options).toBe('object'); + expect(options.url).toBe(targetUrl); + expect(options.method).toBe('POST'); + expect(options.headers['X-GitHub-Event']).toBe(context.eventName); + expect(options.headers['X-Hub-Signature']).toBe('sha1=2aa4571fded2cb5bc29e911b177f5f0d6e0775fa'); + expect(options.headers['X-Hub-Signature-256']).toBe('sha256=34187ae3db37f4e3b61b8b87849737a400be580cd7b05ee81afd5feeb9c3a758'); + expect(options.headers['Content-Type']).toBe('application/json'); + expect(options.body).toBe(JSON.stringify(context.payload, undefined, 2)); + }); +}); diff --git a/action.yml b/action.yml index 4f9cab7..ed69eb6 100644 --- a/action.yml +++ b/action.yml @@ -11,6 +11,9 @@ inputs: webhook-secret: description: 'The secret to use for signing the event payload.' required: false + allow-list-source: + description: 'Location to an allow list of target URL entries.' + required: false runs: using: 'node16' main: 'dist/index.js' diff --git a/dist/index.js b/dist/index.js index ebbd03e..3ea6e8e 100644 --- a/dist/index.js +++ b/dist/index.js @@ -42071,6 +42071,164 @@ function wrappy (fn, cb) { } +/***/ }), + +/***/ 8917: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +// reflector.js + +const request = __nccwpck_require__(1265); +const crypto = __nccwpck_require__(6113); +const URL = (__nccwpck_require__(7310).URL); + +// Function to validate that passed URL is a valid URL +function validateUrl(urlString) { + try { + new URL(urlString); // eslint-disable-line no-new + return true; + } catch (err) { + throw new Error(`Invalid URL: ${urlString} \n ${err}`); + } +} + +// Function to fetch contents of allowListSource and parse into an array +// Source can be a URL or a file path +// Format of allowListSource file is newline separated list of URL patterns +async function fetchAllowListSource(allowListSource) { + const fs = __nccwpck_require__(7147); + + return new Promise((resolve, reject) => { + if (allowListSource.startsWith('http')) { + request(allowListSource, (error, response, body) => { + if (error) { + reject(error); + } else if (response.statusCode < 200 || response.statusCode >= 300) { + reject(new Error(`Error fetching allowListSource: ${allowListSource}: ${response.statusCode} - ${response.statusMessage}`)); + } else { + // Remove comments and split into array based on newline + resolve(body.split('\n').filter((line) => !line.startsWith('#')).filter((line) => line.length > 0)); + } + }); + } else { + fs.readFile(allowListSource, 'utf8', (err, data) => { + if (err) { + reject(err); + } else { + // Remove comments and split into array based on newline + resolve(data.split('\n').filter((line) => !line.startsWith('#')).filter((line) => line.length > 0)); + } + }); + } + }); +} + +// Function to validate that passed target URL is in the passed allowList array via pattern matching +function validateAllowList(targetUrl, allowList) { + let targetUrlObj = new URL(targetUrl); + + for (let i = 0; i < allowList.length; i++) { + let allowListUrlObj = new URL(allowList[i]); + + if (targetUrlObj.hostname === allowListUrlObj.hostname) { + return true; + } + // support for wildcard partial matching in allowList + if (allowListUrlObj.hostname.startsWith('*')) { + let wildcard = allowListUrlObj.hostname.replace('*', ''); + if (targetUrlObj.hostname.endsWith(wildcard)) { + return true; + } + } + } + + return false; +} + +// Function to return webhook signature value +// Specify sha1 or sha256 +function getWebhookSignature(payload, secret, algorithm) { + if (algorithm !== 'sha1' && algorithm !== 'sha256') { + throw new Error(`Invalid algorithm: ${algorithm} \n Must be sha1 or sha256`); + } + + return `${algorithm}=${crypto.createHmac(algorithm, secret).update(payload).digest('hex')}`; +} + +// Function to return Request object with passed context, targetUrl and webhookSecret +function getRequestOptions(context, targetUrl, webhookSecret) { + let payloadJson = JSON.stringify(context.payload, undefined, 2); + + // Build request options + // Include the signature in the headers, if a webhookSecret was provided + let options = { + url: targetUrl, + method: 'POST', + headers: { + 'X-GitHub-Event': context.eventName, + 'Content-Type': 'application/json', + 'Content-Length': context.payload.length, + }, + body: payloadJson, + }; + + // Build GitHub signature headers with secret + if (webhookSecret) { + options.headers['X-Hub-Signature'] = getWebhookSignature(payloadJson, webhookSecret, 'sha1'); + options.headers['X-Hub-Signature-256'] = getWebhookSignature(payloadJson, webhookSecret, 'sha256'); + } + + return options; +} + +// Main Reflector function +async function reflector({context, targetUrl, webhookSecret, allowListSource}) { + // Validate that targetUrl is a valid URL + validateUrl(targetUrl); + + // If allowListSource is provided, fetch the allowList and validate that targetUrl is in the allowList + if (allowListSource) { + let allowList = await fetchAllowListSource(allowListSource); + + if (!validateAllowList(targetUrl, allowList)) { + throw new Error(`targetUrl: ${targetUrl} is not in allowListSource: ${allowListSource}`); + } else { + console.log(`targetUrl: ${targetUrl} is in allowListSource: ${allowListSource}`); + } + } + + // Build request options + let options = getRequestOptions(context, targetUrl, webhookSecret); + + // Send the request + return new Promise((resolve, reject) => { + request(options, (error, response, body) => { + if (error) { + reject(error); + } else if (response.statusCode < 200 || response.statusCode >= 300) { + reject(new Error(`Error sending payload to ${targetUrl}: ${response.statusCode} \n ${response.statusMessage}`)); + } else { + resolve(`Payload sent to ${targetUrl} \n response: ${response.statusCode} - ${response.statusMessage}`); + } + }); + }); +}; + +// Export private functions for testing +const reflectorPrivate = { + validateUrl, + fetchAllowListSource, + validateAllowList, + getWebhookSignature, + getRequestOptions, +}; + +module.exports = { + reflectorPrivate, + reflector, +}; + + /***/ }), /***/ 1214: @@ -42454,71 +42612,23 @@ var __webpack_exports__ = {}; const core = __nccwpck_require__(9991); const github = __nccwpck_require__(6140); -const request = __nccwpck_require__(1265); -const crypto = __nccwpck_require__(6113); +const { reflector } = __nccwpck_require__(8917); // Parse inputs const targetUrl = core.getInput('target-url'); const webhookSecret = core.getInput('webhook-secret'); - -async function reflector({context, targetUrl}) { - let payloadJson = JSON.stringify(context.payload, undefined, 2); - - // Validate that targetUrl is a valid URL - const URL = (__nccwpck_require__(7310).URL); - - function validateUrl(urlString) { - try { - new URL(urlString); // eslint-disable-line no-new - return true; - } catch (err) { - throw new Error(`Invalid targetUrl: ${urlString} \n ${err}`); - } - } - - validateUrl(targetUrl); - - // Build request options - // Include the signature in the headers, if a webhookSecret was provided - let options = { - url: targetUrl, - method: 'POST', - headers: { - 'X-GitHub-Event': context.eventName, - 'Content-Type': 'application/json', - 'Content-Length': context.payload.length, - }, - body: payloadJson, - }; - - // Build GitHub signature headers with secret - if (webhookSecret) { - options.headers['X-Hub-Signature'] = `sha1=${crypto.createHmac('sha1', webhookSecret).update(payloadJson).digest('hex')}`; - options.headers['X-Hub-Signature-256'] = `sha256=${crypto.createHmac('sha256', webhookSecret).update(payloadJson).digest('hex')}`; - } - - // Send the request - return new Promise((resolve, reject) => { - console.log(`Sending payload to ${targetUrl} with options: ${JSON.stringify(options.headers)}`); - - request(options, (error, response, body) => { - if (error) { - reject(error); - } else if (response.statusCode < 200 || response.statusCode >= 300) { - reject(new Error(`Error sending payload to ${targetUrl}: ${response.statusCode} - ${response.statusMessage}`)); - } else { - resolve(`Payload sent to ${targetUrl} \n response: ${response.statusCode} - ${response.statusMessage}`); - } - }); - }); -}; +const allowListSource = core.getInput('allow-list-source'); // Run the Reflector action -reflector({context: github.context, targetUrl: targetUrl}).then((result) => { +reflector({ + context: github.context, + targetUrl: targetUrl, + webhookSecret: webhookSecret, + allowListSource: allowListSource, +}).then((result) => { console.log(result); core.summary - .addHeading('Results') .addRaw(result) .write(); }); diff --git a/index.js b/index.js deleted file mode 100644 index 6c191dd..0000000 --- a/index.js +++ /dev/null @@ -1,72 +0,0 @@ -// index.js - -const core = require('@actions/core'); -const github = require('@actions/github'); -const request = require('request'); -const crypto = require('crypto'); - -// Parse inputs -const targetUrl = core.getInput('target-url'); -const webhookSecret = core.getInput('webhook-secret'); - -async function reflector({context, targetUrl}) { - let payloadJson = JSON.stringify(context.payload, undefined, 2); - - // Validate that targetUrl is a valid URL - const URL = require('url').URL; - - function validateUrl(urlString) { - try { - new URL(urlString); // eslint-disable-line no-new - return true; - } catch (err) { - throw new Error(`Invalid targetUrl: ${urlString} \n ${err}`); - } - } - - validateUrl(targetUrl); - - // Build request options - // Include the signature in the headers, if a webhookSecret was provided - let options = { - url: targetUrl, - method: 'POST', - headers: { - 'X-GitHub-Event': context.eventName, - 'Content-Type': 'application/json', - 'Content-Length': context.payload.length, - }, - body: payloadJson, - }; - - // Build GitHub signature headers with secret - if (webhookSecret) { - options.headers['X-Hub-Signature'] = `sha1=${crypto.createHmac('sha1', webhookSecret).update(payloadJson).digest('hex')}`; - options.headers['X-Hub-Signature-256'] = `sha256=${crypto.createHmac('sha256', webhookSecret).update(payloadJson).digest('hex')}`; - } - - // Send the request - return new Promise((resolve, reject) => { - console.log(`Sending payload to ${targetUrl} with options: ${JSON.stringify(options.headers)}`); - - request(options, (error, response, body) => { - if (error) { - reject(error); - } else if (response.statusCode < 200 || response.statusCode >= 300) { - reject(new Error(`Error sending payload to ${targetUrl}: ${response.statusCode} - ${response.statusMessage}`)); - } else { - resolve(`Payload sent to ${targetUrl} \n response: ${response.statusCode} - ${response.statusMessage}`); - } - }); - }); -}; - -// Run the Reflector action -reflector({context: github.context, targetUrl: targetUrl}).then((result) => { - console.log(result); - - core.summary - .addHeading('Results') - .addRaw(result) - .write(); -}); diff --git a/package-lock.json b/package-lock.json index 6194a41..ac5f3e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "github-actions-reflector", - "version": "0.3.0", + "version": "0.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "github-actions-reflector", - "version": "0.3.0", + "version": "0.4.0", "license": "MIT", "dependencies": { "@actions/core": "^1.10.0", @@ -1056,6 +1056,28 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", @@ -1070,6 +1092,28 @@ "node": ">=10.10.0" } }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -1367,6 +1411,16 @@ } } }, + "node_modules/@jest/reporters/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/@jest/reporters/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -1387,6 +1441,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@jest/schemas": { "version": "29.0.0", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.0.0.tgz", @@ -2070,13 +2136,12 @@ "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -2817,6 +2882,28 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/espree": { "version": "9.4.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", @@ -3209,27 +3296,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.1.tgz", - "integrity": "sha512-362NP+zlprccbEt/SkxKfRMHnNY85V74mVnpUpNyr3F35covl09Kec7/sEFLt3RA4oXmewtoaanoIf67SE5Y5g==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/global-dirs": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", @@ -3788,6 +3854,16 @@ } } }, + "node_modules/jest-config/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/jest-config/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -3808,6 +3884,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/jest-config/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/jest-diff": { "version": "29.3.1", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.3.1.tgz", @@ -4088,6 +4176,16 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-runtime/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/jest-runtime/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4108,6 +4206,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/jest-runtime/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/jest-snapshot": { "version": "29.3.1", "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.3.1.tgz", @@ -4499,15 +4609,15 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=10" } }, "node_modules/ms": { @@ -5069,6 +5179,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/rimraf/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -5089,6 +5209,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5370,6 +5502,16 @@ "node": ">=8" } }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/test-exclude/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -5390,6 +5532,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -6632,6 +6786,27 @@ "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } } }, "@humanwhocodes/config-array": { @@ -6643,6 +6818,27 @@ "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", "minimatch": "^3.0.5" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } } }, "@humanwhocodes/module-importer": { @@ -6873,6 +7069,16 @@ "v8-to-istanbul": "^9.0.1" }, "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -6886,6 +7092,15 @@ "once": "^1.3.0", "path-is-absolute": "^1.0.0" } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } } } }, @@ -7468,13 +7683,12 @@ "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" }, "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "braces": { @@ -7985,6 +8199,27 @@ "strip-ansi": "^6.0.1", "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } } }, "eslint-config-strongloop": { @@ -8303,26 +8538,6 @@ "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "minimatch": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.1.tgz", - "integrity": "sha512-362NP+zlprccbEt/SkxKfRMHnNY85V74mVnpUpNyr3F35covl09Kec7/sEFLt3RA4oXmewtoaanoIf67SE5Y5g==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - } } }, "glob-parent": { @@ -8737,6 +8952,16 @@ "strip-json-comments": "^3.1.1" }, "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -8750,6 +8975,15 @@ "once": "^1.3.0", "path-is-absolute": "^1.0.0" } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } } } }, @@ -8976,6 +9210,16 @@ "strip-bom": "^4.0.0" }, "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -8989,6 +9233,15 @@ "once": "^1.3.0", "path-is-absolute": "^1.0.0" } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } } } }, @@ -9299,12 +9552,12 @@ "dev": true }, "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==", "dev": true, "requires": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" } }, "ms": { @@ -9699,6 +9952,16 @@ "glob": "^7.1.3" }, "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -9712,6 +9975,15 @@ "once": "^1.3.0", "path-is-absolute": "^1.0.0" } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } } } }, @@ -9905,6 +10177,16 @@ "minimatch": "^3.0.4" }, "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -9918,6 +10200,15 @@ "once": "^1.3.0", "path-is-absolute": "^1.0.0" } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } } } }, diff --git a/package.json b/package.json index 3befe87..336232c 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "github-actions-reflector", - "version": "0.3.0", + "version": "0.4.0", "description": "", "main": "app.js", "scripts": { - "test": "eslint --ignore-path .eslintignore . ; cspell *.js *.md", - "build": "ncc build index.js --license licenses.txt" + "test": "eslint --ignore-path .eslintignore . ; cspell *.js *.md ; jest", + "build": "ncc build src/index.js --license licenses.txt" }, "engines": { "npm": ">=8.0.0", diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..8965386 --- /dev/null +++ b/src/index.js @@ -0,0 +1,24 @@ +// index.js + +const core = require('@actions/core'); +const github = require('@actions/github'); +const { reflector } = require('./reflector'); + +// Parse inputs +const targetUrl = core.getInput('target-url'); +const webhookSecret = core.getInput('webhook-secret'); +const allowListSource = core.getInput('allow-list-source'); + +// Run the Reflector action +reflector({ + context: github.context, + targetUrl: targetUrl, + webhookSecret: webhookSecret, + allowListSource: allowListSource, +}).then((result) => { + console.log(result); + + core.summary + .addRaw(result) + .write(); +}); diff --git a/src/reflector.js b/src/reflector.js new file mode 100644 index 0000000..839f503 --- /dev/null +++ b/src/reflector.js @@ -0,0 +1,151 @@ +// reflector.js + +const request = require('request'); +const crypto = require('crypto'); +const URL = require('url').URL; + +// Function to validate that passed URL is a valid URL +function validateUrl(urlString) { + try { + new URL(urlString); // eslint-disable-line no-new + return true; + } catch (err) { + throw new Error(`Invalid URL: ${urlString} \n ${err}`); + } +} + +// Function to fetch contents of allowListSource and parse into an array +// Source can be a URL or a file path +// Format of allowListSource file is newline separated list of URL patterns +async function fetchAllowListSource(allowListSource) { + const fs = require('fs'); + + return new Promise((resolve, reject) => { + if (allowListSource.startsWith('http')) { + request(allowListSource, (error, response, body) => { + if (error) { + reject(error); + } else if (response.statusCode < 200 || response.statusCode >= 300) { + reject(new Error(`Error fetching allowListSource: ${allowListSource}: ${response.statusCode} - ${response.statusMessage}`)); + } else { + // Remove comments and split into array based on newline + resolve(body.split('\n').filter((line) => !line.startsWith('#')).filter((line) => line.length > 0)); + } + }); + } else { + fs.readFile(allowListSource, 'utf8', (err, data) => { + if (err) { + reject(err); + } else { + // Remove comments and split into array based on newline + resolve(data.split('\n').filter((line) => !line.startsWith('#')).filter((line) => line.length > 0)); + } + }); + } + }); +} + +// Function to validate that passed target URL is in the passed allowList array via pattern matching +function validateAllowList(targetUrl, allowList) { + let targetUrlObj = new URL(targetUrl); + + for (let i = 0; i < allowList.length; i++) { + let allowListUrlObj = new URL(allowList[i]); + + if (targetUrlObj.hostname === allowListUrlObj.hostname) { + return true; + } + // support for wildcard partial matching in allowList + if (allowListUrlObj.hostname.startsWith('*')) { + let wildcard = allowListUrlObj.hostname.replace('*', ''); + if (targetUrlObj.hostname.endsWith(wildcard)) { + return true; + } + } + } + + return false; +} + +// Function to return webhook signature value +// Specify sha1 or sha256 +function getWebhookSignature(payload, secret, algorithm) { + if (algorithm !== 'sha1' && algorithm !== 'sha256') { + throw new Error(`Invalid algorithm: ${algorithm} \n Must be sha1 or sha256`); + } + + return `${algorithm}=${crypto.createHmac(algorithm, secret).update(payload).digest('hex')}`; +} + +// Function to return Request object with passed context, targetUrl and webhookSecret +function getRequestOptions(context, targetUrl, webhookSecret) { + let payloadJson = JSON.stringify(context.payload, undefined, 2); + + // Build request options + // Include the signature in the headers, if a webhookSecret was provided + let options = { + url: targetUrl, + method: 'POST', + headers: { + 'X-GitHub-Event': context.eventName, + 'Content-Type': 'application/json', + 'Content-Length': context.payload.length, + }, + body: payloadJson, + }; + + // Build GitHub signature headers with secret + if (webhookSecret) { + options.headers['X-Hub-Signature'] = getWebhookSignature(payloadJson, webhookSecret, 'sha1'); + options.headers['X-Hub-Signature-256'] = getWebhookSignature(payloadJson, webhookSecret, 'sha256'); + } + + return options; +} + +// Main Reflector function +async function reflector({context, targetUrl, webhookSecret, allowListSource}) { + // Validate that targetUrl is a valid URL + validateUrl(targetUrl); + + // If allowListSource is provided, fetch the allowList and validate that targetUrl is in the allowList + if (allowListSource) { + let allowList = await fetchAllowListSource(allowListSource); + + if (!validateAllowList(targetUrl, allowList)) { + throw new Error(`targetUrl: ${targetUrl} is not in allowListSource: ${allowListSource}`); + } else { + console.log(`targetUrl: ${targetUrl} is in allowListSource: ${allowListSource}`); + } + } + + // Build request options + let options = getRequestOptions(context, targetUrl, webhookSecret); + + // Send the request + return new Promise((resolve, reject) => { + request(options, (error, response, body) => { + if (error) { + reject(error); + } else if (response.statusCode < 200 || response.statusCode >= 300) { + reject(new Error(`Error sending payload to ${targetUrl}: ${response.statusCode} \n ${response.statusMessage}`)); + } else { + resolve(`Payload sent to ${targetUrl} \n response: ${response.statusCode} - ${response.statusMessage}`); + } + }); + }); +}; + +// Export private functions for testing +const reflectorPrivate = { + validateUrl, + fetchAllowListSource, + validateAllowList, + getWebhookSignature, + getRequestOptions, +}; + +module.exports = { + reflectorPrivate, + reflector, +};