diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..03d9549
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/jsLinters/eslint.xml b/.idea/jsLinters/eslint.xml
new file mode 100644
index 0000000..58b9bad
--- /dev/null
+++ b/.idea/jsLinters/eslint.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index b66dba7..8f748b7 100644
--- a/README.md
+++ b/README.md
@@ -1,47 +1,88 @@
-
-
# 🛡️ UFW AbuseIPDB Reporter
-A utility designed to analyze UFW firewall logs and report malicious IP addresses to the [AbuseIPDB](https://www.abuseipdb.com) database.
-To prevent redundant reporting of the same IP address within a short period, the tool uses a temporary cache file to track previously reported IPs.
+A utility designed to analyze UFW logs and report IP addresses blocked by the firewall to the [AbuseIPDB](https://www.abuseipdb.com) database.
+To prevent excessive reporting of the same IP address within a short period, the tool uses a temporary cache file to track previously reported IP addresses.
-If you like this repository or find it useful, I would greatly appreciate it if you could give it a star ⭐. Thanks a lot!
-See also this: [sefinek/Cloudflare-WAF-To-AbuseIPDB](https://github.com/sefinek/Cloudflare-WAF-To-AbuseIPDB)
+This project was originally written in Bash but has been rewritten in [Node.js](https://nodejs.org). All my integration tools are now written in Node, hence this change.
+If you were using the old version, please [uninstall it](https://github.com/sefinek/UFW-AbuseIPDB-Reporter/tree/node.js?tab=readme-ov-file#%EF%B8%8F-remove-the-old-version) as it will no longer be supported.
+
+If you like this repository or find it useful, I’d greatly appreciate it if you could give it a star ⭐. Many thanks!
+Also, check this out: [sefinek/Cloudflare-WAF-To-AbuseIPDB](https://github.com/sefinek/Cloudflare-WAF-To-AbuseIPDB)
> [!IMPORTANT]
-> If you'd like to make changes to any files in this repository, please start by creating a [public fork](https://github.com/sefinek/UFW-AbuseIPDB-Reporter/fork).
+> - If you'd like to make changes to any files in this repository, please start by creating a [public fork](https://github.com/sefinek/UFW-AbuseIPDB-Reporter/fork).
+> - According to AbuseIPDB's policy, UDP traffic should not be reported!
## 📋 Requirements
-- **Operating System:** Linux with UFW firewall installed and configured.
-- **AbuseIPDB Account:** An account on the AbuseIPDB service [with a valid API token](https://www.abuseipdb.com/account/api). The API token is required.
-- **Installed packages:**
- - `wget` or `curl`: One of these tools is required to download the [installation script](install.sh) from the GitHub repository and to send requests to the AbuseIPDB API.
- - `jq`: A tool for processing and parsing JSON data returned by the AbuseIPDB API.
- - `openssl`: Used to encode and decode the API token to secure authentication data.
- - `tail`, `awk`, `grep`, `sed`: Standard Unix tools used for text processing and log analysis.
+1. [Node.js + npm](https://nodejs.org)
+2. [PM2](https://www.npmjs.com/package/pm2)
+3. [Git](https://git-scm.com)
-## 🧪 Tested operating systems
-- **Ubuntu Server:** 20.04 & 22.04
-
-*If the distribution you're using to run this tool isn't listed here but works correctly, please create a new [Issue](https://github.com/sefinek/UFW-AbuseIPDB-Reporter/issues) or submit a [Pull request](https://github.com/sefinek/UFW-AbuseIPDB-Reporter/pulls).*
-
+## ✅ Features
+1. A [`config.js`](default.config.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.
## 📥 Installation
-### curl
+
+### Automatic (easy & recommenced)
+#### Via curl
```bash
-bash <(curl -s https://raw.githubusercontent.com/sefinek/UFW-AbuseIPDB-Reporter/main/install.sh)
+bash <(curl -fsS https://raw.githubusercontent.com/sefinek/UFW-AbuseIPDB-Reporter/node.js/install.sh)
```
-### wget
+#### Via wget
```bash
-bash <(wget -qO- https://raw.githubusercontent.com/sefinek/UFW-AbuseIPDB-Reporter/main/install.sh)
+bash <(wget -qO- https://raw.githubusercontent.com/sefinek/UFW-AbuseIPDB-Reporter/node.js/install.sh)
```
-The installation script will automatically download and configure the tool on your machine. During the installation process, you will be prompted to provide an [AbuseIPDB API token](https://www.abuseipdb.com/account/api).
+### Manually
+#### Node.js installation (Ubuntu & Debian)
+```bash
+sudo apt-get 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
+```
+
+#### Git installation
+```bash
+sudo add-apt-repository ppa:git-core/ppa
+sudo apt-get update && sudo apt-get -y install git
+```
+
+#### Script
+```bash
+sudo apt-get update && sudo apt-get upgrade
+cd ~
+git clone https://github.com/sefinek/UFW-AbuseIPDB-Reporter.git
+cd UFW-AbuseIPDB-Reporter
+npm install
+cp default.config.js config.js
+sudo chmod 644 /var/log/ufw.log
+node .
+^C
+npm uninstall corepack -g
+npm install pm2 -g
+sudo mkdir /var/log/ufw-abuseipdb
+sudo chown $USER:$USER /var/log/ufw-abuseipdb -R
+pm2 start
+pm2 startup
+[Paste the command generated by pm2 startup]
+pm2 save
+```
+
+
+## 🗑️ Uninstall the deprecated version if you have it
+```bash
+sudo systemctl stop abuseipdb-ufw.service && sudo systemctl disable abuseipdb-ufw.service
+sudo rm /etc/systemd/system/abuseipdb-ufw.service
+sudo systemctl daemon-reload
+sudo rm -r /usr/local/bin/UFW-AbuseIPDB-Reporter
+```
## 🖥️ Usage
@@ -51,32 +92,33 @@ The tool requires no additional user action after installation. However, it's wo
Servers open to the world are constantly scanned by bots, usually looking for vulnerabilities or other security gaps.
So don't be surprised if the next day, the number of reports to AbuseIPDB exceeds a thousand.
-### 🔍 Checking service status
+### 🔍 Checking logs
```bash
-sudo systemctl status abuseipdb-ufw.service
+pm2 logs ufw-abuseipdb
```
-To see the current logs generated by the process, use the command:
-```bash
-journalctl -u abuseipdb-ufw.service -f
-```
+### 📄 Example reports
+#### 1️⃣
+```text
+Blocked by UFW on homeserver01 [80/tcp]
+Source port: 23639
+TTL: 247
+Packet length: 40
+TOS: 0x00
-### 📄 Example report
-```
-Blocked by UFW (TCP on 80)
-Source port: 28586
-TTL: 116
-Packet length: 48
-TOS: 0x08
-
-This report (for 46.174.191.31) was generated by:
+This report was generated by:
https://github.com/sefinek/UFW-AbuseIPDB-Reporter
```
+#### 2️⃣
+```text
+Blocked by UFW on homeserver01 [30049/tcp]. Generated by: https://github.com/sefinek/UFW-AbuseIPDB-Reporter
+```
+
## 🤝 Development
If you want to contribute to the development of this project, feel free to create a new [Pull request](https://github.com/sefinek/UFW-AbuseIPDB-Reporter/pulls). I will definitely appreciate it!
-## 🔑 GPL-3.0 License
-Copyright 2024 © by [Sefinek](https://sefinek.net). All rights reserved. See the [LICENSE](LICENSE) file for more information.
\ No newline at end of file
+## 🔑 [GPL-3.0 License](LICENSE)
+Copyright 2024-2025 © by [Sefinek](https://sefinek.net). All rights reserved.
\ No newline at end of file
diff --git a/README_PL.md b/README_PL.md
deleted file mode 100644
index fb6880f..0000000
--- a/README_PL.md
+++ /dev/null
@@ -1,82 +0,0 @@
-
-
-# 🛡️ UFW AbuseIPDB Reporter
-Narzędzie zaprojektowane do analizowania dzienników zapory sieciowej UFW i zgłaszania złośliwych adresów IP do bazy danych [AbuseIPDB](https://www.abuseipdb.com).
-Aby zapobiec nadmiarowemu zgłaszaniu tego samego adresu IP w krótkim okresie, narzędzie wykorzystuje tymczasowy plik pamięci podręcznej do śledzenia wcześniej zgłoszonych adresów IP.
-
-Jeśli podoba Ci się to repozytorium lub uważasz je za przydatne, byłbym bardzo wdzięczny, za przyznanie mu gwiazdki ⭐. Wielkie dzięki!
-Zobacz również to: [sefinek/Cloudflare-WAF-To-AbuseIPDB](https://github.com/sefinek/Cloudflare-WAF-To-AbuseIPDB)
-
-> [!IMPORTANT]
-> Jeśli chcesz wprowadzić zmiany do jakichkolwiek pliku z tego repozytorium, zacznij od utworzenia [publicznego forka](https://github.com/sefinek/UFW-AbuseIPDB-Reporter/fork).
-
-
-## 📋 Wymagania
-- **System operacyjny:** Linux z zainstalowanym i skonfigurowanym firewallem UFW.
-- **Konto AbuseIPDB:** Wymagane jest konto w serwisie AbuseIPDB [z ważnym tokenem API](https://www.abuseipdb.com/account/api). Token API jest niezbędny.
-- **Zainstalowane pakiety:**
- - `wget` lub `curl`: Jedno z tych narzędzi jest wymagane do pobrania [skryptu instalacyjnego](install.sh) z repozytorium GitHub oraz do wysyłania zapytań do API AbuseIPDB.
- - `jq`: Narzędzie do przetwarzania i parsowania danych w formacie JSON, zwracanych przez API AbuseIPDB.
- - `openssl`: Używane do kodowania i dekodowania tokena API, aby zabezpieczyć dane uwierzytelniające.
- - `tail`, `awk`, `grep`, `sed`: Standardowe narzędzia Unixowe wykorzystywane do przetwarzania tekstu i analizy logów.
-
-
-## 🧪 Testowane systemy operacyjne
-- **Ubuntu Server:** 20.04 & 22.04
-
-*Jeśli dystrybucja, której używasz do uruchomienia tego narzędzia, nie jest wymieniona tutaj, ale działa poprawnie, utwórz nowy [Issue](https://github.com/sefinek/UFW-AbuseIPDB-Reporter/issues) lub prześlij [Pull request](https://github.com/sefinek/UFW-AbuseIPDB-Reporter/pulls).*
-
-
-## 📥 Instalacja
-### curl
-```bash
-bash <(curl -s https://raw.githubusercontent.com/sefinek/UFW-AbuseIPDB-Reporter/main/install.sh)
-```
-
-### wget
-```bash
-bash <(wget -qO- https://raw.githubusercontent.com/sefinek/UFW-AbuseIPDB-Reporter/main/install.sh)
-```
-
-Skrypt instalacyjny automatycznie pobierze i skonfiguruje narzędzie na komputerze użytkownika. Podczas procesu instalacji zostaniesz poproszony o podanie [tokenu API AbuseIPDB](https://www.abuseipdb.com/account/api).
-
-
-## 🖥️ Użycie
-Po pomyślnej instalacji skrypt będzie działać cały czas w tle, monitorując logi UFW i automatycznie zgłaszając złośliwe adresy IP.
-Narzędzie nie wymaga dodatkowych działań użytkownika po instalacji. Warto jednak od czasu do czasu sprawdzić jego działanie oraz aktualizować skrypt na bieżąco (wywołując polecenie instalacyjne).
-
-Serwery otwarte na świat są nieustannie skanowane przez boty, które zazwyczaj szukają podatności lub jakichkolwiek innych luk w zabezpieczeniach.
-Więc nie zdziw się, jeśli następnego dnia liczba zgłoszeń na AbuseIPDB przekroczy tysiąc.
-
-### 🔍 Sprawdzenie statusu usługi
-```bash
-sudo systemctl status abuseipdb-ufw.service
-```
-
-Aby zobaczyć bieżące logi generowane przez proces, użyj polecenia:
-```bash
-journalctl -u abuseipdb-ufw.service -f
-```
-
-### 📄 Przykładowe zgłoszenie
-```
-Blocked by UFW (TCP on 80)
-Source port: 28586
-TTL: 116
-Packet length: 48
-TOS: 0x08
-
-This report (for 46.174.191.31) was generated by:
-https://github.com/sefinek/UFW-AbuseIPDB-Reporter
-```
-
-
-## 🤝 Rozwój
-Jeśli chcesz przyczynić się do rozwoju tego projektu, śmiało stwórz nowy [Pull request](https://github.com/sefinek/UFW-AbuseIPDB-Reporter/pulls). Z pewnością to docenię!
-
-
-## 🔑 Licencja GPL-3.0
-Copyright 2024 © by [Sefinek](https://sefinek.net). Wszelkie prawa zastrzeżone. Zobacz plik [LICENSE](LICENSE), aby dowiedzieć się więcej.
\ No newline at end of file
diff --git a/default.config.js b/default.config.js
new file mode 100644
index 0000000..3ac4972
--- /dev/null
+++ b/default.config.js
@@ -0,0 +1,83 @@
+exports.MAIN = {
+ // This server
+ 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.
+ REFRESHING_IP_ADDRESS: 5 * 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
+ AUTO_UPDATE_SCHEDULE: '0 18 * * *', // Schedule for automatic script updates (CRON format). Default: every day at 18:00
+ AUTO_UPDATE_ENABLED: true, // Do you want the script to automatically update to the latest version using 'git pull'? (true = enabled, false = disabled)
+};
+
+
+/**
+ * Generates a report submission to AbuseIPDB.
+ * @param {Object} logData
+ * @param {string|null} logData.timestamp
+ * @param {string|null} logData.In
+ * @param {string|null} logData.Out
+ * @param {string|null} logData.srcIp
+ * @param {string|null} logData.dstIp
+ * @param {string|null} logData.res
+ * @param {string|null} logData.tos
+ * @param {string|null} logData.prec
+ * @param {string|null} logData.ttl
+ * @param {string|null} logData.id
+ * @param {string|null} logData.proto
+ * @param {string|null} logData.spt
+ * @param {string|null} logData.dpt
+ * @param {string|null} logData.len
+ * @param {string|null} logData.urgp
+ * @param {string|null} logData.mac
+ * @param {string|null} logData.window
+ * @param {boolean} logData.syn
+ * @param {string|null} fullLog
+ * @param {string|null} serverName
+ * @returns {string} A formatted string report.
+ */
+exports.REPORT_COMMENT = ({ timestamp, In, Out, srcIp, dstIp, res, tos, prec, ttl, id, proto, spt, dpt, len, urgp, mac, window, syn }, fullLog, serverName) =>
+ `Blocked by UFW ${serverName ? `on ${serverName} ` : ''}[${dpt}/${proto?.toLowerCase()}]
+Source port: ${spt || 'N/A'}
+TTL: ${ttl || 'N/A'}
+Packet length: ${len || 'N/A'}
+TOS: ${tos || 'N/A'}
+
+This report was generated by:
+https://github.com/sefinek/UFW-AbuseIPDB-Reporter`; // Please do not remove this URL; I would be very grateful! Thank you. 💙
+
+// Alternative version:
+// exports.REPORT_COMMENT = ({ timestamp, In, Out, srcIp, dstIp, res, tos, prec, ttl, id, proto, spt, dpt, len, urgp, mac, window, syn }, fullLog, serverName) =>
+// `Blocked by UFW ${serverName ? `on ${serverName} ` : ''}[${dpt}/${proto?.toLowerCase()}]. Generated by: https://github.com/sefinek/UFW-AbuseIPDB-Reporter`;
+
+
+// See: https://www.abuseipdb.com/categories
+const categories = {
+ TCP: {
+ 22: '14,22,18', // Port Scan | SSH | Brute-Force
+ 80: '14,21', // Port Scan | Web App Attack
+ 443: '14,21', // Port Scan | Web App Attack
+ 8080: '14,21', // Port Scan | Web App Attack
+ 25: '14,11', // Port Scan | Email Spam
+ 21: '14,5,18', // Port Scan | FTP Brute-Force | Brute-Force
+ 53: '14,1,2', // Port Scan | DNS Compromise | DNS Poisoning
+ 23: '14,15,18', // Port Scan | Hacking | Brute-Force
+ 3389: '14,15,18', // Port Scan | Hacking | Brute-Force
+ 3306: '14,16', // Port Scan | SQL Injection
+ 6666: '14,8', // Port Scan | Fraud VoIP
+ 6667: '14,8', // Port Scan | Fraud VoIP
+ 6668: '14,8', // Port Scan | Fraud VoIP
+ 6669: '14,8', // Port Scan | Fraud VoIP
+ 9999: '14,6', // Port Scan | Ping of Death
+ },
+ UDP: {
+ 53: '14,1,2', // Port Scan | DNS Compromise | DNS Poisoning
+ 123: '14,17', // Port Scan | Spoofing
+ },
+};
+
+exports.DETERMINE_CATEGORIES = ({ proto, dpt }) => categories[proto]?.[dpt] || '14'; // Default: Port Scan
\ No newline at end of file
diff --git a/ecosystem.config.js b/ecosystem.config.js
new file mode 100644
index 0000000..d29f7f6
--- /dev/null
+++ b/ecosystem.config.js
@@ -0,0 +1,26 @@
+module.exports = {
+ apps: [{
+ name: 'ufw-abuseipdb',
+ script: './index.js',
+
+ // Logging configuration
+ log_date_format: 'HH:mm:ss.SSS DD.MM.YYYY',
+ merge_logs: true,
+ log_file: '/var/log/ufw-abuseipdb/combined.log',
+ out_file: '/var/log/ufw-abuseipdb/out.log',
+ error_file: '/var/log/ufw-abuseipdb/error.log',
+
+ // Application restart policy settings
+ wait_ready: true,
+ autorestart: true,
+ max_restarts: 10,
+ min_uptime: 20000,
+ restart_delay: 5000,
+ exp_backoff_restart_delay: 3000,
+
+ // Environment variables configuration
+ env: {
+ NODE_ENV: 'production',
+ },
+ }],
+};
\ No newline at end of file
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 0000000..ae1f169
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,55 @@
+import js from '@eslint/js';
+import globals from 'globals';
+
+export default [
+ js.configs.recommended,
+ {
+ languageOptions: {
+ ecmaVersion: 2024,
+ globals: {
+ ...globals.node,
+ ...globals.es2024,
+ },
+ },
+ rules: {
+ 'arrow-spacing': ['warn', { before: true, after: true }],
+ 'comma-dangle': ['warn', { arrays: 'always-multiline', objects: 'always-multiline' }],
+ 'comma-spacing': 'warn',
+ 'comma-style': 'error',
+ 'curly': ['error', 'multi-line', 'consistent'],
+ 'dot-location': ['error', 'property'],
+ 'handle-callback-err': 'off',
+ 'indent': ['warn', 'tab'],
+ 'keyword-spacing': 'warn',
+ 'max-nested-callbacks': ['error', { max: 4 }],
+ 'max-statements-per-line': ['error', { max: 2 }],
+ 'no-console': 'off',
+ 'no-empty': 'warn',
+ 'no-empty-function': 'warn',
+ 'no-floating-decimal': 'error',
+ 'no-lonely-if': 'warn',
+ 'no-multi-spaces': 'warn',
+ 'no-multiple-empty-lines': ['warn', { max: 3, maxEOF: 1, maxBOF: 0 }],
+ 'no-shadow': ['error', { allow: ['err', 'resolve', 'reject'] }],
+ 'no-trailing-spaces': 'warn',
+ 'no-unreachable': 'warn',
+ 'no-unused-vars': 'warn',
+ 'no-use-before-define': ['error', { functions: false, classes: true }],
+ 'no-var': 'error',
+ 'object-curly-spacing': ['warn', 'always'],
+ 'prefer-const': 'error',
+ 'quotes': ['warn', 'single'],
+ 'semi': ['warn', 'always'],
+ 'sort-vars': 'warn',
+ 'space-before-blocks': 'warn',
+ 'space-before-function-paren': ['warn', { anonymous: 'never', named: 'never', asyncArrow: 'always' }],
+ 'space-in-parens': 'warn',
+ 'space-infix-ops': 'warn',
+ 'space-unary-ops': 'warn',
+ 'spaced-comment': 'warn',
+ 'wrap-regex': 'warn',
+ 'yoda': 'error',
+ },
+ ignores: ['node_modules', '*min.js', '*bundle*', 'build/*', 'dist/*'],
+ },
+];
\ No newline at end of file
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..1be416c
--- /dev/null
+++ b/index.js
@@ -0,0 +1,151 @@
+//
+// Copyright 2024-2025 (c) by Sefinek All rights reserved.
+// https://sefinek.net
+//
+
+const fs = require('node:fs');
+const chokidar = require('chokidar');
+const isLocalIP = require('./utils/isLocalIP.js');
+const { reportedIPs, loadReportedIPs, saveReportedIPs, isIPReportedRecently, markIPAsReported } = require('./services/cache.js');
+const log = require('./utils/log.js');
+const axios = require('./services/axios.js');
+const serverAddress = require('./services/fetchServerIP.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;
+
+let fileOffset = 0;
+
+const reportToAbuseIPDb = async (logData, categories, comment) => {
+ try {
+ const { data: res } = await axios.post('https://api.abuseipdb.com/api/v2/report', new URLSearchParams({
+ ip: logData.srcIp,
+ categories,
+ comment,
+ }), { headers: { 'Key': ABUSEIPDB_API_KEY } });
+
+ log(0, `Reported ${logData.srcIp} [${logData.dpt}/${logData.proto}]; ID: ${logData.id}; Categories ${categories}; Abuse: ${res.data.abuseConfidenceScore}%`);
+ return true;
+ } catch (err) {
+ log(2, `Failed to report ${logData.srcIp} [${logData.dpt}/${logData.proto}]; ID: ${logData.id}; ${err.message}\n${JSON.stringify(err.response.data?.errors || err.response.data)}`);
+ return false;
+ }
+};
+
+const processLogLine = async line => {
+ if (!line.includes('[UFW BLOCK]')) return log(1, `Ignoring line: ${line}`);
+
+ const timestampMatch = line.match(/\[(\d+\.\d+)\]/);
+ const logData = {
+ timestampOld: line.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[+-]\d{2}:\d{2})?/)?.[0] || null,
+ timestampNew: (timestampMatch ? parseFloat(timestampMatch[1]) : null) || null,
+ In: line.match(/IN=([\d.]+)/)?.[1] || null,
+ Out: line.match(/OUT=([\d.]+)/)?.[1] || null,
+ srcIp: line.match(/SRC=([\d.]+)/)?.[1] || null,
+ dstIp: line.match(/DST=([\d.]+)/)?.[1] || null,
+ res: line.match(/RES=(\S+)/)?.[1] || null,
+ tos: line.match(/TOS=(\S+)/)?.[1] || null,
+ prec: line.match(/PREC=(\S+)/)?.[1] || null,
+ ttl: line.match(/TTL=(\d+)/)?.[1] || null,
+ id: line.match(/ID=(\d+)/)?.[1] || null,
+ proto: line.match(/PROTO=(\S+)/)?.[1] || null,
+ spt: line.match(/SPT=(\d+)/)?.[1] || null,
+ dpt: line.match(/DPT=(\d+)/)?.[1] || null,
+ len: line.match(/LEN=(\d+)/)?.[1] || null,
+ urgp: line.match(/URGP=(\d+)/)?.[1] || null,
+ mac: line.match(/MAC=([\w:]+)/)?.[1] || null,
+ window: line.match(/WINDOW=(\d+)/)?.[1] || null,
+ syn: !!line.includes('SYN'),
+ };
+
+ const { srcIp, proto, dpt } = logData;
+ if (!srcIp) {
+ log(1, `Missing SRC in log line: ${line}`);
+ return;
+ }
+
+ if (serverAddress().includes(srcIp)) {
+ log(0, `Ignoring own IP address: ${srcIp}`);
+ return;
+ }
+
+ if (isLocalIP(srcIp)) {
+ log(0, `Ignoring local/private IP: ${srcIp}`);
+ return;
+ }
+
+ // Report MUST NOT be of an attack where the source address is likely spoofed i.e. SYN floods and UDP floods.
+ // TCP connections can only be reported if they complete the three-way handshake. UDP connections cannot be reported.
+ // More: https://www.abuseipdb.com/reporting-policy
+ if (proto === 'UDP') {
+ log(0, `Skipping UDP traffic: SRC=${srcIp} DPT=${dpt}`);
+ return;
+ }
+
+ if (isIPReportedRecently(srcIp)) {
+ const lastReportedTime = reportedIPs.get(srcIp);
+ const elapsedTime = Math.floor(Date.now() / 1000 - lastReportedTime);
+
+ const days = Math.floor(elapsedTime / 86400);
+ const hours = Math.floor((elapsedTime % 86400) / 3600);
+ const minutes = Math.floor((elapsedTime % 3600) / 60);
+ const seconds = elapsedTime % 60;
+
+ const timeAgo = [
+ days && `${days}d`,
+ hours && `${hours}h`,
+ minutes && `${minutes}m`,
+ (seconds || !days && !hours && !minutes) && `${seconds}s`,
+ ].filter(Boolean).join(' ');
+
+ log(0, `${srcIp} was last reported on ${new Date(lastReportedTime * 1000).toLocaleString()} (${timeAgo} ago)`);
+ return;
+ }
+
+ const categories = config.DETERMINE_CATEGORIES(logData);
+ const comment = config.REPORT_COMMENT(logData, line, SERVER_ID);
+
+ if (await reportToAbuseIPDb(logData, categories, comment)) {
+ markIPAsReported(srcIp);
+ saveReportedIPs();
+ }
+};
+
+(async () => {
+ log(0, `v${version} (https://github.com/sefinek/UFW-AbuseIPDB-Reporter)`);
+
+ loadReportedIPs();
+
+ if (!fs.existsSync(UFW_LOG_FILE)) {
+ log(2, `Log file ${UFW_LOG_FILE} does not exist.`);
+ return;
+ }
+
+ fileOffset = fs.statSync(UFW_LOG_FILE).size;
+
+ chokidar.watch(UFW_LOG_FILE, { persistent: true, ignoreInitial: true })
+ .on('change', path => {
+ const stats = fs.statSync(path);
+ if (stats.size < fileOffset) {
+ fileOffset = 0;
+ log(1, 'The file has been truncated, and the offset has been reset.');
+ }
+
+ fs.createReadStream(path, { start: fileOffset, encoding: 'utf8' }).on('data', chunk => {
+ chunk.split('\n').filter(line => line.trim()).forEach(processLogLine);
+ }).on('end', () => {
+ fileOffset = stats.size;
+ });
+ });
+
+ if (process?.send) process.send('ready');
+
+ log(0, `Ready! Now monitoring: ${UFW_LOG_FILE}`);
+ log(0, '=====================================================================');
+
+ // Auto updates
+ if (AUTO_UPDATE_ENABLED) {
+ const autoUpdates = require('./services/updates.js');
+ await autoUpdates.pull();
+ }
+})();
\ No newline at end of file
diff --git a/install.sh b/install.sh
index c4194b2..30fcb5b 100644
--- a/install.sh
+++ b/install.sh
@@ -1,261 +1,243 @@
-#!/bin/bash
+#!/usr/bin/env bash
-###
-# https://github.com/sefinek/UFW-AbuseIPDB-Reporter
-##
-
-VERSION="1.1.2"
-DATE="20.12.2024"
-REPO="https://github.com/sefinek/UFW-AbuseIPDB-Reporter"
+##################################################################
+# Copyright 2024-2025 (c) by Sefinek All rights reserved. #
+# https://sefinek.net #
+##################################################################
cat << "EOF"
- _ _ ___ ____ ____ ____
- / \ | |__ _ _ ___ ___ |_ _| | _ \ | _ \ | __ )
- / _ \ | '_ \ | | | | / __| / _ \ | | | |_) | | | | | | _ \
- / ___ \ | |_) | | |_| | \__ \ | __/ | | | __/ | |_| | | |_) |
- /_/ \_\_|_.__/ _ \__,_| |___/ \___| |___| |_| |____/ |____/
+ _ _ ___ ____ ____ ____
+ / \ | |__ _ _ ___ ___ |_ _| | _ \ | _ \ | __ )
+ / _ \ | '_ \ | | | | / __| / _ \ | | | |_) | | | | | | _ \
+ / ___ \ | |_) | | |_| | \__ \ | __/ | | | __/ | |_| | | |_) |
+ /_/ \_\_|_.__/ _ \__,_| |___/ \___| |___| |_| |____/ |____/
- (_)_ __ | |_ ___ __ _ _ __ __ _| |_(_) ___ _ __
- | | '_ \| __/ _ \/ _` | '__/ _` | __| |/ _ \| '_ \
- | | | | | || __/ (_| | | | (_| | |_| | (_) | | | |
- |_|_| |_|\__\___|\__, |_| \__,_|\__|_|\___/|_| |_|
- |___/
+ (_)_ __ | |_ ___ __ _ _ __ __ _| |_(_) ___ _ __
+ | | '_ \| __/ _ \/ _` | '__/ _` | __| |/ _ \| '_ \
+ | | | | | || __/ (_| | | | (_| | |_| | (_) | | | |
+ |_|_| |_|\__\___|\__, |_| \__,_|\__|_|\___/|_| |_|
+ |___/
+
+ >> Made by sefinek.net || Last update: 18.01.2025 <<
+
+This installer will configure UFW-AbuseIPDB-Reporter, a tool that analyzes UFW logs and
+reports to AbuseIPDB the IP addresses that have violated firewall rules. Join my Discord
+server to stay updated on the latest changes and more: https://discord.gg/53DBjTuzgZ
+============================================================================================
EOF
-cat <> Made by sefinek.net || Version: $VERSION [$DATE] <<
-
-This installer will configure UFW-AbuseIPDB-Reporter, a tool that analyzes
-UFW firewall logs and reports IP addresses to AbuseIPDB. Remember to perform
-updates periodically. You can join my Discord server to receive notifications
-about the latest changes and more: https://discord.gg/53DBjTuzgZ
-================================================================================
-
-EOF
-
-# Function to download a file using either curl or wget
-download_file() {
- local url="$1"
- local output="$2"
- local user_agent="UFW-AbuseIPDB-Reporter/$VERSION (+$REPO)"
-
- if command -v curl >/dev/null 2>&1; then
- echo "INFO: Using 'curl' to download the file..."
- sudo curl -A "$user_agent" -o "$output" "$url"
- elif command -v wget >/dev/null 2>&1; then
- echo "INFO: 'curl' is not installed. Using 'wget' to download the file..."
- sudo wget --header="User-Agent: $user_agent" -O "$output" "$url"
- else
- echo "FAIL: Neither 'curl' nor 'wget' is installed! Please install one of these packages and try running the script again."
- exit 1
- fi
-}
-
-# Ask
-ask_user() {
- local question="$1"
- local response
-
+# Function to prompt for a Yes/no answer
+yes_no_prompt() {
+ local prompt="$1"
while true; do
- read -rp "$ $question [Yes/no]: " response
- case "${response,,}" in
- yes|y) return 0;;
- no|n) return 1;;
- *) echo "Invalid input. Please answer 'yes' or 'no'."
- echo;;
+ read -r -p "$prompt [Yes/no]: " answer
+ case $answer in
+ [Yy]*|[Yy]es ) return 0 ;; # Return 0 for Yes
+ [Nn]*|[Nn]o ) return 1 ;; # Return 1 for No
+ * ) echo "❌ Invalid input. Please answer Yes/no or Y/n." ;;
esac
done
}
-# ========================= CHECK FOR MISSING PACKAGES =========================
-required_packages=(ufw jq openssl)
-missing_packages=()
+# Function to check and install missing dependencies
+check_dependencies() {
+ local dependencies=(curl node git)
+ local missing=()
-for pkg in "${required_packages[@]}"; do
- if ! dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q "install ok installed"; then
- missing_packages+=("$pkg")
- fi
-done
+ for dependency in "${dependencies[@]}"; do
+ if ! command -v "$dependency" &> /dev/null; then
+ missing+=("$dependency")
+ else
+ echo "✅ $dependency is installed ($(command -v "$dependency"))"
+ if $dependency --version &> /dev/null; then
+ $dependency --version
+ else
+ echo "ℹ️ Version information for $dependency is unavailable"
+ fi
+ fi
+ done
-if [ ${#missing_packages[@]} -gt 0 ]; then
- echo "WARN: The following packages are not installed: ${missing_packages[*]}"
- if ! ask_user "Do you want to install them now?"; then
- echo "FAIL: Missing dependencies packages. Installation cannot proceed without them."
- exit 1
- fi
-
- echo "INFO: Installing missing dependencies: ${missing_packages[*]}"
- if ! sudo apt-get update && sudo apt-get install -y "${missing_packages[@]}"; then
- echo "FAIL: Failed to install the required dependencies. Aborting installation!"
- exit 1
- fi
-
- echo -e "INFO: All required dependencies have been successfully installed.\n"
-else
- echo "INFO: Dependencies are already installed on this machine."
-fi
-
-
-# =========================== Check if the service already exists ===========================
-if systemctl list-unit-files | grep -q '^abuseipdb-ufw.service'; then
- echo "WARN: abuseipdb-ufw.service is already installed! If you plan to update or reinstall, choose 'Yes'."
- if ask_user "Do you want to remove the existing service?"; then
- sudo systemctl stop abuseipdb-ufw.service
- sudo systemctl disable abuseipdb-ufw.service
- sudo rm /etc/systemd/system/abuseipdb-ufw.service
- sudo systemctl daemon-reload
+ if [[ ${#missing[@]} -gt 0 ]]; then
+ echo "🚨 Missing dependencies: ${missing[*]}"
+ for dep in "${missing[@]}"; do
+ if yes_no_prompt "📦 Do you want to install $dep?"; then
+ case $dep in
+ curl ) sudo apt-get install -y curl ;;
+ node ) curl -fsSL https://deb.nodesource.com/setup_22.x -o nodesource_setup.sh && sudo bash nodesource_setup.sh && sudo apt-get install -y nodejs && rm -f nodesource_setup.sh ;;
+ git ) sudo add-apt-repository ppa:git-core/ppa && sudo apt-get update && sudo apt-get -y install git ;;
+ esac
+ else
+ echo "❌ Cannot proceed without $dep. Exiting..."
+ exit 1
+ fi
+ done
else
- echo -e "INFO: Existing service will not be removed\n"
+ echo "✅ All dependencies are installed"
fi
-fi
+}
+# Check dependencies before proceeding
+check_dependencies
-# =========================== Prepare installation directory ===========================
-install_dir="/usr/local/bin/UFW-AbuseIPDB-Reporter"
-script_path="$install_dir/reporter.sh"
-if [ -d "$install_dir" ]; then
- echo "INFO: Directory $install_dir already exists. Removing it..."
- if ! sudo rm -rf "$install_dir"; then
- echo "FAIL: Something went wrong. Failed to remove existing directory $install_dir."
+# Function to validate AbuseIPDB API key
+validate_token() {
+ local api_key=$1
+ local api_url="https://api.abuseipdb.com/api/v2/check?ipAddress=8.8.8.8"
+ local response
+
+ if command -v curl &>/dev/null; then
+ response=$(curl -s -o /dev/null -w "%{http_code}" -H "Key: $api_key" "$api_url")
+ elif command -v wget &>/dev/null; then
+ response=$(wget --quiet --server-response --header="Key: $api_key" --output-document=/dev/null "$api_url" 2>&1 | awk '/HTTP\/1\.[01] [0-9]{3}/ {print $2}' | tail -n1)
+ else
+ echo "🚨 Neither curl nor wget is installed. Please install one of them to proceed."
exit 1
fi
+
+ if [[ $response -eq 200 ]]; then
+ echo "✅ Yay! Token is valid."
+ return 0
+ else
+ echo "❌ Invalid token! Please try again."
+ return 1
+ fi
+}
+
+# Check for UFW log file
+if [[ ! -f /var/log/ufw.log ]]; then
+ read -r -p "🔍 /var/log/ufw.log not found. Please enter the path to your log file: " ufw_log_path
+ if [[ -f $ufw_log_path ]]; then
+ echo "✅ Log file found at $ufw_log_path"
+ else
+ echo "❌ Provided log file path does not exist. Exiting..."
+ exit 1
+ fi
+else
+ ufw_log_path="/var/log/ufw.log"
+ echo "✅ /var/log/ufw.log exists"
fi
-echo "INFO: Creating installation directory at $install_dir..."
-if ! sudo mkdir -p "$install_dir"; then
- echo "FAIL: Something went wrong. Failed to create installation directory."
- exit 1
-fi
-echo
-
-
-# =========================== Prepare reporter.sh script ===========================
-GITHUB_URL="https://raw.githubusercontent.com/sefinek/UFW-AbuseIPDB-Reporter/main/reporter.sh"
-echo "INFO: Downloading reporter.sh from $GITHUB_URL..."
-if ! download_file "$GITHUB_URL" "$script_path"; then
- echo "FAIL: Something went wrong while downloading the file from GitHub servers! Maybe try running this script as sudo?"
- exit 1
-fi
-echo "INFO: Saved reporter.sh at location $script_path"
-
-if ! sudo chmod +x "$script_path"; then
- echo "FAIL: Failed to make reporter.sh executable."
- exit 1
-fi
-echo -e "INFO: reporter.sh has been made executable.\n"
-
-
-# =========================== AbuseIPDB API token ===========================
-max_attempts=4
-valid_token=false
-for ((attempts = 0; attempts < max_attempts; attempts++)); do
- read -rsp "$ Please enter your AbuseIPDB API token: " api_key
- echo
-
- if [[ "$api_key" =~ ^[a-f0-9]{80}$ ]]; then
- valid_token=true
+# Prompt for AbuseIPDB API token
+while true; do
+ read -r -p "🔑 Please enter your AbuseIPDB API token: " api_token
+ if validate_token "$api_token"; then
break
fi
+ continue
+done
- attempts_left=$((max_attempts - attempts - 1))
- if (( attempts_left > 0 )); then
- echo "WARN: Invalid API token format. Please enter an 80-character hexadecimal string. You have $attempts_left/$max_attempts attempts left."
+# Prompt for server ID
+while true; do
+ read -r -p "🖥️ Enter the server ID (e.g., homeserver1). Leave blank if you do not wish to provide one: " server_id
+ if [[ -z $server_id ]]; then
+ server_id=null
+ break
+ elif [[ $server_id =~ ^[A-Za-z0-9]{1,16}$ ]]; then
+ break
+ else
+ echo "❌ It must be 1-16 characters long, contain only letters and numbers, and have no spaces or special characters."
fi
done
-if [[ "$valid_token" != true ]]; then
- echo "FAIL: Maximum number of attempts reached. Installation aborted!"
- exit 1
+# Prompt for system update and upgrade
+if yes_no_prompt "🛠️ Do you want the script to run apt update and apt upgrade for you?"; then
+ echo "🔧 Updating and upgrading the system..."
+ sudo apt-get update && sudo apt-get upgrade
fi
-# Encode the API token
-token_file="$install_dir/.abuseipdb_token"
-echo "INFO: Encoding data (file $token_file)..."
-if ! echo -n "$api_key" | openssl enc -base64 | sudo tee "$token_file" >/dev/null; then
- echo "FAIL: Something went wrong. Failed to encode API token."
- exit 1
-fi
-
-# Update the ENCODED_API_KEY_FILE variable in reporter.sh by replacing the existing definition
-echo "INFO: Updating ENCODED_API_KEY_FILE variable in reporter.sh..."
-if ! sudo sed -i "s|^ENCODED_API_KEY_FILE=.*|ENCODED_API_KEY_FILE=\"$token_file\"|" "$script_path"; then
- echo "FAIL: Failed to update ENCODED_API_KEY_FILE in reporter.sh."
- exit 1
-fi
-
-echo "INFO: Setting permissions (chmod 644) for /var/log/ufw.log..."
-sudo chmod 644 /var/log/ufw.log
-echo
-
-
-# =========================== abuseipdb-ufw.service ===========================
-if ask_user "Would you like to add reporter.sh as a service and start it?"; then
- service_file="/etc/systemd/system/abuseipdb-ufw.service"
- echo "INFO: Setting up reporter.sh as a service"
- if ! sudo bash -c "cat > $service_file" <<-EOF
-[Unit]
-Description=UFW AbuseIPDB Reporter
-After=network.target
-Documentation=$REPO
-
-[Service]
-Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
-ExecStart=$script_path
-Restart=always
-User=$(logname)
-WorkingDirectory=$install_dir
-StandardOutput=journal
-StandardError=journal
-
-[Install]
-WantedBy=multi-user.target
-EOF
- then
- echo "FAIL: Failed to create service file. Please check your permissions!"
- exit 1
- fi
-
- sudo systemctl daemon-reload
-
- if sudo systemctl enable abuseipdb-ufw.service && sudo systemctl start abuseipdb-ufw.service; then
- echo "INFO: Attempting to start the abuseipdb-ufw.service..."
- else
- echo "FAIL: Failed to enable or start the abuseipdb-ufw.service. Please check the system logs for details."
- exit 1
- fi
-
- echo "INFO: Waiting 8 seconds to verify the script's stability..."
- sleep 8
-
- if sudo systemctl is-active --quiet abuseipdb-ufw.service; then
- echo "INFO: abuseipdb-ufw.service is running!"
- sudo systemctl status abuseipdb-ufw.service --no-pager
- else
- echo "FAIL: abuseipdb-ufw.service failed to start."
- sudo systemctl status abuseipdb-ufw.service --no-pager
- exit 1
- fi
+# Clone repository & set permissions
+if [ ! -d "/opt" ]; then
+ mkdir -p /opt
+ echo "📂 '/opt' has been created"
else
- echo -e "INFO: reporter.sh will not be added as a service. Running the script directly... Press ^C to stop.\n"
- if "$script_path"; then
- echo "INFO: reporter.sh executed successfully."
- else
- echo "FAIL: Failed to execute reporter.sh!"
- exit 1
- fi
+ echo "✅ '/opt' directory already exists"
fi
-echo
-# Prompt to add the service to autostart
-if ask_user "Do you want to add abuseipdb-ufw.service to autostart?"; then
- if sudo systemctl enable abuseipdb-ufw.service; then
- echo "INFO: Great! abuseipdb-ufw.service has been added to autostart. Installation finished!"
- echo "INFO: Run 'journalctl -u abuseipdb-ufw.service -f' to view more logs."
- else
- echo "FAIL: Failed to add abuseipdb-ufw.service to autostart!"
- exit 1
- fi
+cd /opt || { echo "❌ Failed to change directory to '/opt'. Exiting..."; exit 1; }
+
+if [ ! -d "UFW-AbuseIPDB-Reporter" ]; then
+ echo "📥 Cloning the UFW-AbuseIPDB-Reporter repository..."
+ sudo git clone https://github.com/sefinek/UFW-AbuseIPDB-Reporter.git --branch node.js || { echo "❌ Failed to clone the repository. Exiting..."; exit 1; }
else
- echo "INFO: abuseipdb-ufw.service will not be added to autostart. Installation finished!"
+ echo "✨ The UFW-AbuseIPDB-Reporter repository already exists"
fi
+
+sudo chown "$USER":"$USER" /opt/UFW-AbuseIPDB-Reporter -R
+
+echo "📥 Pulling latest changes..."
+cd UFW-AbuseIPDB-Reporter || { echo "❌ Failed to change directory to 'UFW-AbuseIPDB-Reporter'. Exiting..."; exit 1; }
+git pull || { echo "❌ Failed to pull the latest changes. Exiting..."; exit 1; }
+
+# Install npm dependencies
+echo "📦 Installing npm dependencies..."
+npm install -silent
+
+# Copy configuration file
+if [ -e config.js ]; then
+ echo "✅ config.js already exists"
+else
+ echo "📑 Copying default.config.js to config.js..."
+ cp default.config.js config.js
+fi
+
+# Update config.js with API token, Server ID, and UFW log path
+config_file="config.js"
+if [[ -f $config_file ]]; then
+ echo "🔧 Updating $PWD/$config_file..."
+ sed -i "s|UFW_LOG_FILE: .*|UFW_LOG_FILE: '$ufw_log_path',|" $config_file
+ sed -i "s|SERVER_ID: .*|SERVER_ID: '$server_id',|" $config_file
+ sed -i "s|ABUSEIPDB_API_KEY: .*|ABUSEIPDB_API_KEY: '$api_token',|" $config_file
+else
+ echo "❌ $config_file not found. Make sure the repository was cloned and initialized correctly."
+ exit 1
+fi
+
+# Create logs directory
+echo "📂 Creating /var/log/ufw-abuseipdb directory..."
+sudo mkdir -p /var/log/ufw-abuseipdb
+sudo chown "$USER":"$USER" /var/log/ufw-abuseipdb -R
+
+# Change permissions for UFW log file
+echo "🔒 Changing permissions for $ufw_log_path..."
+sudo chmod 644 "$ufw_log_path"
+
+# Install pm2
+echo "📦 Installing PM2..."
+sudo npm install pm2 -g -silent
+
+
+# Configure PM2
+echo "⚙️ Adding PM2 to autostart..."
+startup_command=$(pm2 startup | grep "sudo env PATH" | sed 's/^[^s]*sudo/sudo/')
+
+if [ -n "$startup_command" ]; then
+ echo "⚙️ Executing: $startup_command"
+ eval "$startup_command" &>/dev/null || {
+ echo "❌ Failed to execute the startup command!"
+ }
+else
+ echo "❌ Failed to find the command generated by pm2 startup! PM2 was not added to autostart."
+fi
+
+echo "⚙️ Running a script with PM2 and saving the current state of all processes managed by it..."
+pm2 start --silent
+pm2 save --silent
+
+
+# Final
+echo "🌌 Checking PM2 status..."
+pm2 status
+
+echo -e "\n🎉 Installation and configuration completed! Use the 'pm2 logs' command to monitor logs in real time."
+
+echo -e "\n====================================== Summary ======================================"
+echo "🖥️ Server ID : ${server_id:-null}"
+echo "🔑 API Key : $api_token"
+echo "📂 Script : $PWD"
+echo "⚙️ Config File : $PWD/config.js"
+
+echo -e "\n====================================== Support ======================================"
+echo "📩 Email : contact@sefinek.net"
+echo "🔵 Discord : https://discord.gg/RVH8UXgmzs"
+echo "😺 GitHub Issues : https://github.com/sefinek/UFW-AbuseIPDB-Reporter/issues"
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..959f999
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,261 @@
+{
+ "name": "ufw-abuseipdb-reporter",
+ "version": "0.1.2",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "ufw-abuseipdb-reporter",
+ "version": "0.1.2",
+ "license": "GPL-3.0",
+ "dependencies": {
+ "axios": "^1.7.9",
+ "chokidar": "^4.0.3",
+ "cron": "^3.5.0",
+ "ipaddr.js": "^2.2.0",
+ "simple-git": "^3.27.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.19.0",
+ "globals": "^15.14.0"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.19.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz",
+ "integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@kwsites/file-exists": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz",
+ "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.1"
+ }
+ },
+ "node_modules/@kwsites/promise-deferred": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz",
+ "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/luxon": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
+ "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==",
+ "license": "MIT"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.7.9",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
+ "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/cron": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/cron/-/cron-3.5.0.tgz",
+ "integrity": "sha512-0eYZqCnapmxYcV06uktql93wNWdlTmmBFP2iYz+JPVcQqlyFYcn1lFuIk4R54pkOmE7mcldTAPZv6X5XA4Q46A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/luxon": "~3.4.0",
+ "luxon": "~3.5.0"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+ "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.9",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
+ "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/globals": {
+ "version": "15.14.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz",
+ "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ipaddr.js": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
+ "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/luxon": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz",
+ "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
+ "node_modules/readdirp": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz",
+ "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.18.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/simple-git": {
+ "version": "3.27.0",
+ "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.27.0.tgz",
+ "integrity": "sha512-ivHoFS9Yi9GY49ogc6/YAi3Fl9ROnF4VyubNylgCkA+RVqLaKWnDSzXOVzya8csELIaWaYNutsEuAhZrtOjozA==",
+ "license": "MIT",
+ "dependencies": {
+ "@kwsites/file-exists": "^1.1.1",
+ "@kwsites/promise-deferred": "^1.1.1",
+ "debug": "^4.3.5"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/steveukx/git-js?sponsor=1"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..e028517
--- /dev/null
+++ b/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "ufw-abuseipdb-reporter",
+ "version": "0.1.2",
+ "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",
+ "bugs": {
+ "url": "https://github.com/sefinek/UFW-AbuseIPDB-Reporter/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/sefinek/UFW-AbuseIPDB-Reporter.git"
+ },
+ "license": "GPL-3.0",
+ "author": "Sefinek (https://sefinek.net)",
+ "type": "commonjs",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "up": "ncu -u && npm install && npm update && npm audit fix"
+ },
+ "dependencies": {
+ "axios": "^1.7.9",
+ "chokidar": "^4.0.3",
+ "cron": "^3.5.0",
+ "ipaddr.js": "^2.2.0",
+ "simple-git": "^3.27.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.19.0",
+ "globals": "^15.14.0"
+ }
+}
diff --git a/pull.txt b/pull.txt
new file mode 100644
index 0000000..54c50e3
--- /dev/null
+++ b/pull.txt
@@ -0,0 +1 @@
+git pull && pm2 restart ufw-abuseipdb && pm2 logs ufw-abuseipdb
\ No newline at end of file
diff --git a/reporter.sh b/reporter.sh
deleted file mode 100644
index e572710..0000000
--- a/reporter.sh
+++ /dev/null
@@ -1,222 +0,0 @@
-#!/bin/bash
-
-###
-# https://github.com/sefinek/UFW-AbuseIPDB-Reporter
-##
-
-LOG_FILE="/var/log/ufw.log"
-ENCODED_API_KEY_FILE="./.abuseipdb_token"
-REPORTED_IPS_FILE="/tmp/ufw-abuseipdb-reporter.cache"
-REPORT_INTERVAL=43200 # 12h (in seconds)
-
-declare -A reported_ips
-
-log() {
- local level="$1"
- local message="$2"
- echo "[$level] $message"
-}
-
-# Check if the API key file exists and decode it
-if [[ -f "$ENCODED_API_KEY_FILE" ]]; then
- DECODED_API_KEY=$(openssl enc -d -base64 -in "$ENCODED_API_KEY_FILE")
- if [[ -z "$DECODED_API_KEY" ]]; then
- log "ERROR" "Failed to decode API key from $ENCODED_API_KEY_FILE"
- exit 1
- fi
-else
- log "ERROR" "API key file not found at $ENCODED_API_KEY_FILE"
- exit 1
-fi
-
-ABUSEIPDB_API_KEY="$DECODED_API_KEY"
-
-# Check if jq, curl, or wget packages are available
-if ! command -v jq &> /dev/null; then
- log "ERROR" "jq is not installed. Please install jq to run this script."
- exit 1
-fi
-
-if ! command -v curl &> /dev/null && ! command -v wget &> /dev/null; then
- log "ERROR" "Neither curl nor wget is available. Please install one of them to continue."
- exit 1
-fi
-
-load_reported_ips() {
- if [[ -f "$REPORTED_IPS_FILE" ]]; then
- while IFS= read -r line; do
- [[ -z "$line" ]] && continue
- IFS=' ' read -r ip report_time <<< "$line"
- if [[ -n "$ip" && -n "$report_time" ]]; then
- reported_ips["$ip"]=$report_time
- else
- log "WARN" "Invalid line format: '$line'"
- fi
- done < "$REPORTED_IPS_FILE"
- log "INFO" "Loaded ${#reported_ips[@]} IPs from $REPORTED_IPS_FILE"
- else
- log "INFO" "$REPORTED_IPS_FILE does not exist. No data to load."
- fi
-}
-
-save_reported_ips() {
- : > "$REPORTED_IPS_FILE"
- for ip in "${!reported_ips[@]}"; do
- echo "$ip ${reported_ips[$ip]}" >> "$REPORTED_IPS_FILE"
- done
-}
-
-is_local_ip() {
- local ip="$1"
- [[ "$ip" =~ ^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|127\.|fc|fd|fe80|::1) ]]
-}
-
-report_to_abuseipdb() {
- local ip="$1" categories="$2" proto="$3" spt="$4" dpt="$5" ttl="$6" len="$7" tos="$8" timestamp="$9"
-
- local comment="Blocked by UFW ($proto on $dpt)
-Source port: $spt"
-
- [[ -n "$ttl" ]] && comment+="
-TTL: $ttl"
-
- [[ -n "$len" ]] && comment+="
-Packet length: $len"
-
- [[ -n "$tos" ]] && comment+="
-TOS: $tos"
-
- comment+="
-
-This report (for $ip) was generated by:
-https://github.com/sefinek/UFW-AbuseIPDB-Reporter" # Please do not remove the URL to the repository of this script. I would be really grateful. 💙
-
- local res
- if command -v curl >/dev/null 2>&1; then
- res=$(curl -s -X POST "https://api.abuseipdb.com/api/v2/report" \
- --data-urlencode "ip=$ip" \
- --data-urlencode "categories=$categories" \
- --data-urlencode "comment=$comment" \
- -H "Key: $ABUSEIPDB_API_KEY" \
- -H "Accept: application/json")
- elif command -v wget >/dev/null 2>&1; then
- res=$(wget -qO- --post-data="ip=$ip&categories=$categories&comment=$comment" \
- --header="Key: $ABUSEIPDB_API_KEY" \
- --header="Accept: application/json" \
- "https://api.abuseipdb.com/api/v2/report")
- else
- log "ERROR" "Neither curl nor wget is available to send the report."
- return 1
- fi
-
- local abuse_confidence_score
- abuse_confidence_score=$(echo "$res" | jq -r '.data.abuseConfidenceScore')
-
- if [[ "$abuse_confidence_score" =~ ^[0-9]+$ ]]; then
- log "INFO" "Successfully reported IP $ip to AbuseIPDB (score $abuse_confidence_score)"
- return 0
- else
- log "ERROR" "Failed to report IP $ip to AbuseIPDB: $res"
- return 1
- fi
-}
-
-is_ip_reported_recently() {
- local ip="$1"
- local current_time
- current_time=$(date +%s)
-
- if [[ -v reported_ips["$ip"] ]]; then
- local report_time=${reported_ips["$ip"]}
- (( current_time - report_time < REPORT_INTERVAL )) && return 0
- fi
- return 1
-}
-
-mark_ip_as_reported() {
- local ip="$1"
- reported_ips["$ip"]=$(date +%s)
-}
-
-determine_categories() {
- local proto="$1"
- local dpt="$2"
-
- # See: https://www.abuseipdb.com/categories
- case "$proto" in
- "TCP")
- case "$dpt" in
- 22) echo "14,22,18" ;; # Port Scan | SSH | Brute-Force
- 80 | 443 | 8080) echo "14,21" ;; # Port Scan | Web App Attack
- 25) echo "14,11" ;; # Port Scan | Email Spam
- 21) echo "14,5,18" ;; # Port Scan | FTP Brute-Force | Brute-Force
- 53) echo "14,1,2" ;; # Port Scan | DNS Compromise | DNS Poisoning
- 23 | 3389) echo "14,15,18" ;; # Port Scan | Hacking | Brute-Force
- 3306) echo "14,16" ;; # Port Scan | SQL Injection
- 6666 | 6667 | 6668 | 6669) echo "14,8" ;; # Port Scan | Fraud VoIP
- 9999) echo "14,6" ;; # Port Scan | Ping of Death
- *) echo "14" ;; # Port Scan
- esac
- ;;
- "UDP")
- case "$dpt" in
- 53) echo "14,1,2" ;; # Port Scan | DNS Compromise | DNS Poisoning
- 123) echo "14,17" ;; # Port Scan | Spoofing
- *) echo "14" ;; # Port Scan
- esac
- ;;
- *) echo "14" ;; # Port Scan
- esac
-}
-
-process_log_line() {
- local line="$1"
- if [[ "$line" == *"[UFW BLOCK]"* ]]; then
- local timestamp src_ip proto spt dpt ttl len tos categories
-
- timestamp=$(echo "$line" | awk '{print $1, $2, $3}')
- [[ -z "$timestamp" ]] && timestamp=$(date '+%Y-%m-%d %H:%M:%S')
- src_ip=$(echo "$line" | grep -oP 'SRC=\K[^\s]+')
-
- if is_local_ip "$src_ip"; then
- log "INFO" "Ignoring local IP: $src_ip"
- return
- fi
-
- proto=$(echo "$line" | grep -oP 'PROTO=\K[^\s]+')
- spt=$(echo "$line" | grep -oP 'SPT=\K[^\s]+')
- dpt=$(echo "$line" | grep -oP 'DPT=\K[^\s]+')
- ttl=$(echo "$line" | grep -oP 'TTL=\K[^\s]+')
- len=$(echo "$line" | grep -oP 'LEN=\K[^\s]+')
- tos=$(echo "$line" | grep -oP 'TOS=\K[^\s]+')
-
- # Report MUST NOT be of an attack where the source address is likely spoofed i.e. SYN floods and UDP floods.
- # TCP connections can only be reported if they complete the three-way handshake. UDP connections cannot be reported.
- # More: https://www.abuseipdb.com/reporting-policy
- if [[ "$proto" == "UDP" ]]; then
- log "INFO" "Skipping UDP traffic: SRC=$src_ip DPT=$dpt"
- return
- fi
-
- if is_ip_reported_recently "$src_ip"; then
- log "INFO" "IP $src_ip ($proto) was reported recently"
- return
- fi
-
- categories=$(determine_categories "$proto" "$dpt")
-
- log "INFO" "Reporting IP $src_ip ($proto $dpt) with categories $categories..."
- if report_to_abuseipdb "$src_ip" "$categories" "$proto" "$spt" "$dpt" "$ttl" "$len" "$tos" "$timestamp"; then
- mark_ip_as_reported "$src_ip"
- save_reported_ips
- fi
- fi
-}
-
-load_reported_ips
-
-log "INFO" "Starting to monitor $LOG_FILE"
-
-tail -Fn0 "$LOG_FILE" | while read -r line; do
- process_log_line "$line"
-done
diff --git a/services/axios.js b/services/axios.js
new file mode 100644
index 0000000..c329bae
--- /dev/null
+++ b/services/axios.js
@@ -0,0 +1,13 @@
+const axios = require('axios');
+const { version } = require('../package.json');
+
+axios.defaults.headers.common = {
+ 'User-Agent': `Mozilla/5.0 (compatible; UFW-AbuseIPDB-Reporter/${version}; +https://github.com/sefinek/UFW-AbuseIPDB-Reporter)`,
+ 'Accept': 'application/json',
+ 'Cache-Control': 'no-cache',
+ 'Connection': 'keep-alive',
+};
+
+axios.defaults.timeout = 30000;
+
+module.exports = axios;
\ No newline at end of file
diff --git a/services/cache.js b/services/cache.js
new file mode 100644
index 0000000..d2150ee
--- /dev/null
+++ b/services/cache.js
@@ -0,0 +1,30 @@
+const { existsSync, readFileSync, writeFileSync } = require('node:fs');
+const { CACHE_FILE, IP_REPORT_COOLDOWN } = require('../config.js').MAIN;
+const log = require('../utils/log.js');
+
+const reportedIPs = new Map();
+
+const loadReportedIPs = () => {
+ if (existsSync(CACHE_FILE)) {
+ readFileSync(CACHE_FILE, 'utf8')
+ .split('\n')
+ .forEach(line => {
+ const [ip, time] = line.split(' ');
+ if (ip && time) reportedIPs.set(ip, Number(time));
+ });
+ log(0, `Loaded ${reportedIPs.size} IPs from ${CACHE_FILE}`);
+ } else {
+ log(0, `${CACHE_FILE} does not exist. No data to load.`);
+ }
+};
+
+const saveReportedIPs = () => writeFileSync(CACHE_FILE, Array.from(reportedIPs).map(([ip, time]) => `${ip} ${time}`).join('\n'), 'utf8');
+
+const isIPReportedRecently = ip => {
+ const reportedTime = reportedIPs.get(ip);
+ return reportedTime && (Date.now() / 1000 - reportedTime < IP_REPORT_COOLDOWN / 1000);
+};
+
+const markIPAsReported = ip => reportedIPs.set(ip, Math.floor(Date.now() / 1000));
+
+module.exports = { reportedIPs, loadReportedIPs, saveReportedIPs, isIPReportedRecently, markIPAsReported };
\ No newline at end of file
diff --git a/services/fetchServerIP.js b/services/fetchServerIP.js
new file mode 100644
index 0000000..af23c45
--- /dev/null
+++ b/services/fetchServerIP.js
@@ -0,0 +1,40 @@
+const { networkInterfaces } = require('node:os');
+const axios = require('./axios.js');
+const isLocalIP = require('../utils/isLocalIP.js');
+const { REFRESHING_IP_ADDRESS } = require('../config.js').MAIN;
+
+const ipAddrList = new Set();
+
+const fetchIPv4Address = async () => {
+ try {
+ const { data } = await axios.get('https://api.sefinek.net/api/v2/ip');
+ if (data?.success && data?.message) ipAddrList.add(data.message);
+ } catch (err) {
+ console.warn('Error fetching IPv4 address:', err.message);
+ }
+};
+
+const fetchIPv6Address = () => {
+ try {
+ Object.values(networkInterfaces()).flat().forEach(({ address, internal }) => {
+ if (!internal && address && !isLocalIP(address)) ipAddrList.add(address);
+ });
+ } catch (err) {
+ console.warn('Error fetching IPv6 address:', err.message);
+ }
+};
+
+const fetchServerIPs = async () => {
+ ipAddrList.clear();
+ await fetchIPv4Address();
+ fetchIPv6Address();
+};
+
+(async () => {
+ await fetchServerIPs();
+ setInterval(fetchServerIPs, REFRESHING_IP_ADDRESS);
+
+ // console.debug(ipAddrList);
+})();
+
+module.exports = () => Array.from(ipAddrList);
\ No newline at end of file
diff --git a/services/reloadApp.js b/services/reloadApp.js
new file mode 100644
index 0000000..67f5167
--- /dev/null
+++ b/services/reloadApp.js
@@ -0,0 +1,19 @@
+const { exec } = require('node:child_process');
+const ecosystem = require('../ecosystem.config.js');
+
+const executeCommand = cmd =>
+ new Promise((resolve, reject) => {
+ exec(cmd, (err, stdout, stderr) => {
+ if (err || stderr) reject(err || stderr);
+ else resolve(stdout);
+ });
+ });
+
+module.exports = async () => {
+ try {
+ console.log(await executeCommand('npm install --omit=dev'));
+ console.log(await executeCommand(`pm2 restart ${ecosystem.apps[0].name}`));
+ } catch (err) {
+ console.error(err);
+ }
+};
\ No newline at end of file
diff --git a/services/updates.js b/services/updates.js
new file mode 100644
index 0000000..d353d82
--- /dev/null
+++ b/services/updates.js
@@ -0,0 +1,29 @@
+const { AUTO_UPDATE_SCHEDULE } = require('../config.js').MAIN;
+
+const simpleGit = require('simple-git');
+const { CronJob } = require('cron');
+const restartApp = require('./reloadApp.js');
+const log = require('../utils/log.js');
+
+const git = simpleGit();
+
+const pull = async () => {
+ log(0, '$ git pull');
+
+ const { summary } = await git.pull();
+ log(0, `Changes: ${summary.changes}; Deletions: ${summary.insertions}; Insertions: ${summary.insertions};`);
+};
+
+const pullAndRestart = async () => {
+ try {
+ await pull();
+ await restartApp();
+ } catch (err) {
+ log(2, err.message);
+ }
+};
+
+// https://crontab.guru
+new CronJob(AUTO_UPDATE_SCHEDULE || '0 18 * * *', pullAndRestart, null, true, 'UTC'); // At 18:00
+
+module.exports = { pull };
\ No newline at end of file
diff --git a/utils/isLocalIP.js b/utils/isLocalIP.js
new file mode 100644
index 0000000..7d7ab80
--- /dev/null
+++ b/utils/isLocalIP.js
@@ -0,0 +1,10 @@
+const ipaddr = require('ipaddr.js');
+
+module.exports = ip => {
+ const range = ipaddr.parse(ip).range();
+ return [
+ 'unspecified', 'multicast', 'linkLocal', 'loopback', 'reserved', 'benchmarking',
+ 'amt', 'broadcast', 'carrierGradeNat', 'private', 'as112', 'uniqueLocal',
+ 'ipv4Mapped', 'rfc6145', '6to4', 'teredo', 'as112v6', 'orchid2', 'droneRemoteIdProtocolEntityTags',
+ ].includes(range);
+};
\ No newline at end of file
diff --git a/utils/log.js b/utils/log.js
new file mode 100644
index 0000000..b11361d
--- /dev/null
+++ b/utils/log.js
@@ -0,0 +1,10 @@
+const levels = {
+ 0: { method: 'log', label: '[INFO]' },
+ 1: { method: 'warn', label: '[WARN]' },
+ 2: { method: 'error', label: '[FAIL]' },
+};
+
+module.exports = (level, msg) => {
+ const { method, label } = levels[level] || { method: 'log', label: '[N/A]' };
+ console[method](`${label} ${msg}`);
+};
\ No newline at end of file