diff --git a/README.md b/README.md index 60cfb29..648199a 100644 --- a/README.md +++ b/README.md @@ -21,16 +21,19 @@ Also, check this out: [sefinek/Cloudflare-WAF-To-AbuseIPDB](https://github.com/s ## ✅ Features -1. A [`config.js`](config.default.js) file enabling easy configuration. -2. A simple installer allowing quick integration deployment. -3. Integration with Discord Webhooks (coming soon): - - Alerts in case of script errors - - Daily summaries of reported IP addresses -4. Automatic updates. +1. **Easy Configuration** – The [`config.js`](config.default.js) file allows for quick and simple customization. +2. **Simple Installer** – Enables fast and seamless integration deployment. +3. **Self-IP Protection** – The script will never report an IP address belonging to you or your server, even if you use a dynamic IP. +4. **Discord Webhooks Integration**: + - Important notifications. + - Alerts for script errors. + - Daily summaries of reported IP addresses. +5. **Automatic Updates** – The script regularly fetches and applies the latest updates. If you want, you can disable it, of course. + ## 📥 Installation (Ubuntu & Debian) -### Automatic (easy & recommenced) +### Automatic (easy & fast & recommenced) #### Via curl ```bash bash <(curl -fsS https://raw.githubusercontent.com/sefinek/UFW-AbuseIPDB-Reporter/main/install.sh) @@ -44,20 +47,20 @@ bash <(wget -qO- https://raw.githubusercontent.com/sefinek/UFW-AbuseIPDB-Reporte ### Manually #### Node.js installation ```bash -sudo apt-get install -y curl +sudo apt install -y curl curl -fsSL https://deb.nodesource.com/setup_22.x -o nodesource_setup.sh -sudo -E bash nodesource_setup.sh && sudo apt-get install -y nodejs +sudo -E bash nodesource_setup.sh && sudo apt install -y nodejs ``` #### Git installation ```bash sudo add-apt-repository ppa:git-core/ppa -sudo apt-get update && sudo apt-get -y install git +sudo apt update && sudo apt -y install git ``` #### Commands ```bash -sudo apt-get update && sudo apt-get upgrade +sudo apt update && sudo apt upgrade cd ~ git clone https://github.com/sefinek/UFW-AbuseIPDB-Reporter.git cd UFW-AbuseIPDB-Reporter diff --git a/config.default.js b/config.default.js index 23a93c7..8171536 100644 --- a/config.default.js +++ b/config.default.js @@ -3,15 +3,19 @@ exports.MAIN = { UFW_LOG_FILE: '/var/log/ufw.log', CACHE_FILE: '/tmp/ufw-abuseipdb-reporter.cache', SERVER_ID: null, // The server name that will be visible in the reports (e.g., 'homeserver1'). If you don't want to define it, leave the value as null. - IP_REFRESH_INTERVAL: 8 * 60 * 1000, // How often should (every 5 minutes) the script check the server's IP address to avoid accidental self-reports? + IP_REFRESH_INTERVAL: 10 * 60 * 1000, // How often should (every 5 minutes) the script check the server's IP address to avoid accidental self-reports? // Reporting ABUSEIPDB_API_KEY: '', // Secret API key for AbuseIPDB. IP_REPORT_COOLDOWN: 12 * 60 * 60 * 1000, // The minimum time (12 hours) that must pass before reporting the same IP address again. - // Automatic updates + // Automatic Updates AUTO_UPDATE_ENABLED: true, // Do you want the script to automatically update to the latest version using 'git pull'? (true = enabled, false = disabled) AUTO_UPDATE_SCHEDULE: '0 18 * * *', // Schedule for automatic script updates (CRON format). Default: every day at 18:00 + + // Discord Webhooks + DISCORD_WEBHOOKS_ENABLED: false, + DISCORD_WEBHOOKS_URL: '', }; diff --git a/index.js b/index.js index 1be416c..769b546 100644 --- a/index.js +++ b/index.js @@ -10,9 +10,10 @@ const { reportedIPs, loadReportedIPs, saveReportedIPs, isIPReportedRecently, mar const log = require('./utils/log.js'); const axios = require('./services/axios.js'); const serverAddress = require('./services/fetchServerIP.js'); +const discordWebhooks = require('./services/discord.js'); const config = require('./config.js'); const { version } = require('./package.json'); -const { UFW_LOG_FILE, ABUSEIPDB_API_KEY, SERVER_ID, AUTO_UPDATE_ENABLED } = config.MAIN; +const { UFW_LOG_FILE, ABUSEIPDB_API_KEY, SERVER_ID, AUTO_UPDATE_ENABLED, AUTO_UPDATE_SCHEDULE, DISCORD_WEBHOOKS_ENABLED, DISCORD_WEBHOOKS_URL } = config.MAIN; let fileOffset = 0; @@ -33,7 +34,7 @@ const reportToAbuseIPDb = async (logData, categories, comment) => { }; const processLogLine = async line => { - if (!line.includes('[UFW BLOCK]')) return log(1, `Ignoring line: ${line}`); + if (!line.includes('[UFW BLOCK]')) return log(0, `Ignoring line: ${line}`); const timestampMatch = line.match(/\[(\d+\.\d+)\]/); const logData = { @@ -143,9 +144,9 @@ const processLogLine = async line => { log(0, `Ready! Now monitoring: ${UFW_LOG_FILE}`); log(0, '====================================================================='); + await discordWebhooks(0, `[UFW-AbuseIPDB-Reporter](https://github.com/sefinek/UFW-AbuseIPDB-Reporter) has been successfully launched on the device \`${SERVER_ID}\`.`); + // Auto updates - if (AUTO_UPDATE_ENABLED) { - const autoUpdates = require('./services/updates.js'); - await autoUpdates.pull(); - } + if (AUTO_UPDATE_ENABLED && AUTO_UPDATE_SCHEDULE) await require('./services/updates.js')(); + if (DISCORD_WEBHOOKS_ENABLED && DISCORD_WEBHOOKS_URL) await require('./services/summaries.js')(); })(); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 556f5d6..3e41e63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ufw-abuseipdb-reporter", - "version": "0.1.3", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ufw-abuseipdb-reporter", - "version": "0.1.3", + "version": "0.2.0", "license": "GPL-3.0", "dependencies": { "axios": "^1.7.9", diff --git a/package.json b/package.json index 11c2398..807a63b 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "ufw-abuseipdb-reporter", - "version": "0.1.3", + "version": "0.2.0", "description": "A tool (with a simple installer) that monitors UFW firewall logs in real time and reports IP addresses to the AbuseIPDB database.", "keywords": [ "ufw", "abuseipdb" ], - "homepage": "https://github.com/sefinek/UFW-AbuseIPDB-Reporter", + "homepage": "https://github.com/sefinek/UFW-AbuseIPDB-Reporter#readme", "bugs": { "url": "https://github.com/sefinek/UFW-AbuseIPDB-Reporter/issues" }, diff --git a/services/discord.js b/services/discord.js new file mode 100644 index 0000000..5f4f28c --- /dev/null +++ b/services/discord.js @@ -0,0 +1,45 @@ +const axios = require('axios'); +const log = require('../utils/log.js'); +const { SERVER_ID, DISCORD_WEBHOOKS_ENABLED, DISCORD_WEBHOOKS_URL } = require('../config.js').MAIN; + +const TYPES = { + 0: { type: 'SUCCESS', emoji: '\\✅', color: 0x60D06D }, + 1: { type: 'WARN', emoji: '\\⚠️', color: 0xFFB02E }, + 2: { type: 'ERROR', emoji: '\\❌', color: 0xF92F60 }, + 3: { type: 'FAIL', emoji: '\\🔴', color: 0xF8312F }, + 4: { type: 'INFO', emoji: '\\📄', color: 0xF2EEF8 }, + 5: { type: 'DEBUG', emoji: '\\🛠️', color: 0xB4ACBC }, + 6: { type: 'CRITICAL', emoji: '\\🔴', color: 0xF8312F }, + 7: { type: 'NOTICE', emoji: '\\📝', color: 0xF3EEF8 }, +}; + +module.exports = async (id, description) => { + if (!DISCORD_WEBHOOKS_ENABLED || !DISCORD_WEBHOOKS_URL) return; + + const logType = TYPES[id]; + if (!logType) return log(1, 'Invalid log type ID provided!'); + + const config = { + method: 'POST', + url: DISCORD_WEBHOOKS_URL, + headers: { 'Content-Type': 'application/json' }, + data: { + embeds: [{ + title: `${logType.emoji} ${SERVER_ID}: ${logType.type} [ID ${id}]`, + description, + color: logType.color, + footer: { + text: `Date: ${new Date().toLocaleString()} | sefinek/UFW-AbuseIPDB-Reporter`, + }, + timestamp: new Date().toISOString(), + }], + }, + }; + + try { + const res = await axios(config); + if (res.status !== 204) log(1, 'Failed to deliver Discord Webhook'); + } catch (err) { + log(2, `Failed to send Discord Webhook! ${err.stack}`); + } +}; \ No newline at end of file diff --git a/services/reloadApp.js b/services/reloadApp.js index 67f5167..9d03217 100644 --- a/services/reloadApp.js +++ b/services/reloadApp.js @@ -1,7 +1,9 @@ const { exec } = require('node:child_process'); const ecosystem = require('../ecosystem.config.js'); +const discordWebhooks = require('./discord.js'); +const log = require('../utils/log.js'); -const executeCommand = cmd => +const executeCmd = cmd => new Promise((resolve, reject) => { exec(cmd, (err, stdout, stderr) => { if (err || stderr) reject(err || stderr); @@ -10,10 +12,13 @@ const executeCommand = cmd => }); module.exports = async () => { + const process = ecosystem.apps[0].name; + await discordWebhooks(4, `Restarting the ${process} process...`); + try { - console.log(await executeCommand('npm install --omit=dev')); - console.log(await executeCommand(`pm2 restart ${ecosystem.apps[0].name}`)); + console.log(await executeCmd('npm install --omit=dev')); + console.log(await executeCmd(`pm2 restart ${process}`)); } catch (err) { - console.error(err); + log(2, err); } }; \ No newline at end of file diff --git a/services/summaries.js b/services/summaries.js new file mode 100644 index 0000000..501a07e --- /dev/null +++ b/services/summaries.js @@ -0,0 +1,73 @@ +const { CronJob } = require('cron'); +const fs = require('node:fs/promises'); +const discordWebhooks = require('./discord.js'); +const log = require('../utils/log.js'); +const { CACHE_FILE } = require('../config.js').MAIN; + +const formatHourRange = hour => `${hour.toString().padStart(2, '0')}:00-${hour.toString().padStart(2, '0')}:59`; +const pluralizeReport = count => (count === 1 ? 'report' : 'reports'); + +const sendWebhook = async () => { + try { + await fs.access(CACHE_FILE); + } catch { + return log(2, `Cache file not found: ${CACHE_FILE}`); + } + + let data; + try { + data = (await fs.readFile(CACHE_FILE, 'utf8')).trim(); + } catch (err) { + return log(2, `Error reading file: ${err.message}`); + } + + if (!data) { + log(0, `Cache file is empty: ${CACHE_FILE}`); + return discordWebhooks(4, `Cache file is empty: \`${CACHE_FILE}\``); + } + + try { + const yesterday = new Date(); + yesterday.setUTCDate(yesterday.getUTCDate() - 1); + const yesterdayString = yesterday.toISOString().split('T')[0]; + + const hourlySummary = {}; + const uniqueEntries = new Set(); + + data.split('\n').forEach((line) => { + const [ip, timestamp] = line.split(' '); + if (!ip || isNaN(timestamp)) return; + + const entryKey = `${ip}_${timestamp}`; + if (uniqueEntries.has(entryKey)) return; + uniqueEntries.add(entryKey); + + const dateObj = new Date(parseInt(timestamp, 10) * 1000); + if (dateObj.toISOString().split('T')[0] !== yesterdayString) return; + + const hour = dateObj.getUTCHours(); + hourlySummary[hour] = (hourlySummary[hour] || 0) + 1; + }); + + const totalReports = Object.values(hourlySummary).reduce((sum, count) => sum + count, 0); + const sortedEntries = Object.entries(hourlySummary).sort((a, b) => b[1] - a[1]); + const maxReports = sortedEntries.length > 0 ? sortedEntries[0][1] : 0; + const topHours = sortedEntries + .filter(([, count]) => count === maxReports && count > 1) + .map(([hour]) => parseInt(hour)); + + const summaryString = Object.entries(hourlySummary) + .map(([hour, count]) => `${formatHourRange(parseInt(hour))}: ${count} ${pluralizeReport(count)}${topHours.includes(parseInt(hour)) ? ' 🔥' : ''}`) + .join('\n'); + + await discordWebhooks(7, `Midnight. Summary of IP address reports (${totalReports}) from yesterday (${yesterdayString}).\nGood night to you, sleep well! 😴\n\`\`\`${summaryString}\`\`\``); + log(0, `Reported IPs yesterday by hour:\n${summaryString}\nTotal reported IPs: ${totalReports} ${pluralizeReport(totalReports)}`); + } catch (err) { + log(2, err); + } +}; + +module.exports = async () => { + await sendWebhook(); + new CronJob('0 0 * * *', sendWebhook, null, true, 'UTC'); +}; diff --git a/services/updates.js b/services/updates.js index d353d82..42980cb 100644 --- a/services/updates.js +++ b/services/updates.js @@ -4,14 +4,21 @@ const simpleGit = require('simple-git'); const { CronJob } = require('cron'); const restartApp = require('./reloadApp.js'); const log = require('../utils/log.js'); +const discordWebhooks = require('./discord.js'); const git = simpleGit(); const pull = async () => { + await discordWebhooks(4, 'Updating the local repository in progress `(git pull)`...'); log(0, '$ git pull'); - const { summary } = await git.pull(); - log(0, `Changes: ${summary.changes}; Deletions: ${summary.insertions}; Insertions: ${summary.insertions};`); + try { + const { summary } = await git.pull(); + log(0, `Changes: ${summary.changes}; Deletions: ${summary.insertions}; Insertions: ${summary.insertions}`); + await discordWebhooks(4, `**Changes:** ${summary.changes}; **Deletions:** ${summary.insertions}; **Insertions:** ${summary.insertions}`); + } catch (err) { + log(2, err); + } }; const pullAndRestart = async () => { @@ -19,11 +26,11 @@ const pullAndRestart = async () => { await pull(); await restartApp(); } catch (err) { - log(2, err.message); + log(2, err); } }; // https://crontab.guru -new CronJob(AUTO_UPDATE_SCHEDULE || '0 18 * * *', pullAndRestart, null, true, 'UTC'); // At 18:00 +new CronJob(AUTO_UPDATE_SCHEDULE, pullAndRestart, null, true, 'UTC'); -module.exports = { pull }; \ No newline at end of file +module.exports = pull; \ No newline at end of file diff --git a/utils/log.js b/utils/log.js index b11361d..c743e7c 100644 --- a/utils/log.js +++ b/utils/log.js @@ -1,3 +1,5 @@ +const discordWebhooks = require('../services/discord.js'); + const levels = { 0: { method: 'log', label: '[INFO]' }, 1: { method: 'warn', label: '[WARN]' }, @@ -7,4 +9,6 @@ const levels = { module.exports = (level, msg) => { const { method, label } = levels[level] || { method: 'log', label: '[N/A]' }; console[method](`${label} ${msg}`); + + if (level >= 1) discordWebhooks(level, msg).catch(console.error); }; \ No newline at end of file