diff --git a/.env.example b/.env.example index a9fb3ae..9d1e332 100644 --- a/.env.example +++ b/.env.example @@ -8,3 +8,6 @@ FAST_MODE=0 # Multiline is supported: https://github.com/motdotla/dotenv#multiline-values # ALLOWLIST_URLS= # BLOCKLIST_URLS= + +# Optional Discord webhook URL to send a message to when a script is done running or if an error occurs. +# DISCORD_WEBHOOK_URL= diff --git a/README.md b/README.md index 001b478..9d02744 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Cloudflare Gateway allows you to create custom rules to filter HTTP, DNS, and ne 3. Cloudflare email, API key (NOT the API token), and account ID 4. A file containing the domains you want to block - **max 300,000 domains for the free plan** - in the working directory named `blocklist.txt`. Mullvad provides awesome [DNS blocklists](https://github.com/mullvad/dns-blocklists) that work well with this project. A script that downloads recommended blocklists, `download_lists.js`, is included. 5. Optional: You can whitelist domains by putting them in a file `allowlist.txt`. You can also use the `get_recomended_whitelist.sh` Bash script to get the recommended whitelists. +6. Optional: A Discord (or similar) webhook URL to send notifications to. ### Running locally @@ -55,6 +56,7 @@ Please note that the GitHub Action downloads the recommended blocklists and whit - `CLOUDFLARE_ACCOUNT_ID`: Your Cloudflare account ID - `CLOUDFLARE_LIST_ITEM_LIMIT`: The maximum number of blocked domains allowed for your Cloudflare Zero Trust plan. Default to 300,000. Optional if you are using the free plan. - `PING_URL`: /Optional/ The HTTP(S) URL to ping (using curl) after the GitHub Action has successfully updated your filters. Useful for monitoring. +- `DISCORD_WEBHOOK_URL`: /Optional/ The Discord (or similar) webhook URL to send notifications to. Good for monitoring as well. 3. Create the following GitHub Actions variables in your repository settings if you desire: diff --git a/auto_update_github_action.yml b/auto_update_github_action.yml index 47a4311..fc1bf67 100644 --- a/auto_update_github_action.yml +++ b/auto_update_github_action.yml @@ -49,6 +49,7 @@ jobs: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} CLOUDFLARE_LIST_ITEM_LIMIT: ${{ secrets.CLOUDFLARE_LIST_ITEM_LIMIT }} + DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} FAST_MODE: ${{ vars.FAST_MODE }} - name: Create new rules and lists @@ -57,6 +58,7 @@ jobs: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} CLOUDFLARE_LIST_ITEM_LIMIT: ${{ secrets.CLOUDFLARE_LIST_ITEM_LIMIT }} + DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} FAST_MODE: ${{ vars.FAST_MODE }} - name: Send ping request diff --git a/cf_gateway_rule_create.js b/cf_gateway_rule_create.js index 9bf8f34..3e7bc83 100644 --- a/cf_gateway_rule_create.js +++ b/cf_gateway_rule_create.js @@ -1,4 +1,5 @@ import { createZeroTrustRule, getZeroTrustLists } from "./lib/api.js"; +import { notifyWebhook } from "./lib/helpers.js"; const { result: lists } = await getZeroTrustLists(); const wirefilterExpression = lists.reduce((previous, current) => { @@ -10,3 +11,5 @@ const wirefilterExpression = lists.reduce((previous, current) => { console.log("Creating rule..."); // Remove the trailing ' or ' await createZeroTrustRule(wirefilterExpression.slice(0, -4)); +// Send a notification to the webhook +await notifyWebhook("CF Gateway Rule Create script finished running"); diff --git a/cf_gateway_rule_delete.js b/cf_gateway_rule_delete.js index a0e08f4..67e9050 100644 --- a/cf_gateway_rule_delete.js +++ b/cf_gateway_rule_delete.js @@ -1,4 +1,5 @@ import { deleteZeroTrustRule, getZeroTrustRules } from "./lib/api.js"; +import { notifyWebhook } from "./lib/helpers.js"; const { result: rules } = await getZeroTrustRules(); const cgpsRule = rules.find(({ name }) => name === "CGPS Filter Lists"); @@ -14,3 +15,5 @@ const cgpsRule = rules.find(({ name }) => name === "CGPS Filter Lists"); console.log(`Deleting rule ${cgpsRule.name}...`); await deleteZeroTrustRule(cgpsRule.id); })(); +// Send a notification to the webhook +await notifyWebhook("CF Gateway Rule Create script finished running"); diff --git a/cf_list_create.js b/cf_list_create.js index 52a8b0b..68670cb 100644 --- a/cf_list_create.js +++ b/cf_list_create.js @@ -12,7 +12,7 @@ import { LIST_ITEM_SIZE, PROCESSING_FILENAME, } from "./lib/constants.js"; -import { normalizeDomain } from "./lib/helpers.js"; +import { normalizeDomain, notifyWebhook } from "./lib/helpers.js"; import { extractDomain, isComment, @@ -143,8 +143,12 @@ console.log("\n\n"); if (FAST_MODE) { await createZeroTrustListsAtOnce(domains); + // TODO: make this less repetitive + await notifyWebhook(`CF List Create script finished running (${domains.length} domains, ${numberOfLists} lists)`); return; } await createZeroTrustListsOneByOne(domains); + + await notifyWebhook(`CF List Create script finished running (${domains.length} domains, ${numberOfLists} lists)`); })(); diff --git a/cf_list_delete.js b/cf_list_delete.js index f8be9e4..34e4de5 100644 --- a/cf_list_delete.js +++ b/cf_list_delete.js @@ -4,6 +4,7 @@ import { getZeroTrustLists, } from "./lib/api.js"; import { FAST_MODE } from "./lib/constants.js"; +import { notifyWebhook } from "./lib/helpers.js"; (async () => { const { result: lists } = await getZeroTrustLists(); @@ -32,8 +33,12 @@ import { FAST_MODE } from "./lib/constants.js"; if (FAST_MODE) { await deleteZeroTrustListsAtOnce(cgpsLists); + // TODO: make this less repetitive + await notifyWebhook(`CF List Delete script finished running (${cgpsLists.length} lists)`); return; } await deleteZeroTrustListsOneByOne(cgpsLists); + + await notifyWebhook(`CF List Delete script finished running (${cgpsLists.length} lists)`); })(); diff --git a/lib/helpers.js b/lib/helpers.js index 7c92111..8911626 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -19,6 +19,53 @@ if (!globalThis.fetch) { globalThis.fetch = (await import("node-fetch")).default; } +/** + * Sends a message to a Discord-compatible webhook. + * @param {url|string} url The webhook URL. + * @param {string} message The message to be sent. + * @returns {Promise} + */ +async function sendMessageToWebhook(url, message) { + // Create the payload object with the message + const payload = { content: message }; + + // Send a POST request to the webhook url with the payload as JSON + try { + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + + // Check if the request was successful + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } else { + return true; + } + } catch (error) { + console.error('Error sending message to webhook:', error); + return false; + } +} + +/** + * Sends a CGPS notification to a Discord-compatible webhook. + * Automatically checks if the webhook URL exists. + * @param {string} msg The message to be sent. + * @returns {Promise} + */ +export async function notifyWebhook(msg) { + // Check if the webhook URL exists + const webhook_url = process.env.DISCORD_WEBHOOK_URL; + + if (webhook_url || !webhook_url.startsWith('http')) { + // Send the message to the webhook + await sendMessageToWebhook(webhook_url, `CGPS: ${msg}`); + } + // Not logging the lack of a webhook URL since it's not a feature everyone would use +} + /** * Fires request to the specified URL. * @param {string} url The URL to which the request will be fired. @@ -53,6 +100,8 @@ const request = async (url, options) => { }); if (!response.ok) { + // Send a message to the Discord webhook if it exists + await notifyWebhook(`An HTTP error has occurred (${response.status}) while making a web request. Please check the logs for further details.`); throw new Error(`HTTP error! Status: ${response.status}`); }