diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6da04de
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+config.js
\ No newline at end of file
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/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml
new file mode 100644
index 0000000..d23208f
--- /dev/null
+++ b/.idea/jsLibraryMappings.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..3606594 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,3 @@
-
-
# 🛡️ 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.
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..1c88b22
--- /dev/null
+++ b/default.config.js
@@ -0,0 +1,20 @@
+exports.MAIN = {
+ LOG_FILE: 'D:\\test\\ufw.log',
+ CACHE_FILE: 'D:\\test\\ufw-abuseipdb-reporter.cache',
+
+ ABUSEIPDB_API_KEY: '',
+ GITHUB_REPO: 'https://github.com/sefinek/UFW-AbuseIPDB-Reporter',
+
+ REPORT_INTERVAL: 43200,
+};
+
+exports.REPORT_COMMENT = (timestamp, srcIp, dstIp, proto, spt, dpt, ttl, len, tos) => {
+ return `Blocked by UFW (${proto} on ${dpt})
+Source port: ${spt}
+TTL: ${ttl || 'N/A'}
+Packet length: ${len || 'N/A'}
+TOS: ${tos || 'N/A'}
+
+This report (for ${srcIp}) 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. 💙
+};
\ No newline at end of file
diff --git a/ecosystem.config.js b/ecosystem.config.js
new file mode 100644
index 0000000..a099545
--- /dev/null
+++ b/ecosystem.config.js
@@ -0,0 +1 @@
+module.exports = {};
\ No newline at end of file
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 0000000..1b986f0
--- /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': [1, { before: true, after: true }],
+ 'comma-dangle': [1, { arrays: 'always-multiline', objects: 'always-multiline' }],
+ 'comma-spacing': 1,
+ 'comma-style': 'error',
+ 'curly': ['error', 'multi-line', 'consistent'],
+ 'dot-location': ['error', 'property'],
+ 'handle-callback-err': 'off',
+ 'indent': [1, 'tab'],
+ 'keyword-spacing': 1,
+ 'max-nested-callbacks': ['error', { max: 4 }],
+ 'max-statements-per-line': ['error', { max: 2 }],
+ 'no-console': 'off',
+ 'no-empty': 1,
+ 'no-empty-function': 1,
+ 'no-floating-decimal': 'error',
+ 'no-lonely-if': 1,
+ 'no-multi-spaces': 1,
+ 'no-multiple-empty-lines': [1, { max: 3, maxEOF: 1, maxBOF: 0 }],
+ 'no-shadow': ['error', { allow: ['err', 'resolve', 'reject'] }],
+ 'no-trailing-spaces': 1,
+ 'no-unreachable': 1,
+ 'no-unused-vars': 1,
+ 'no-use-before-define': ['error', { functions: false, classes: true }],
+ 'no-var': 'error',
+ 'object-curly-spacing': [1, 'always'],
+ 'prefer-const': 'error',
+ 'quotes': [1, 'single'],
+ 'semi': [1, 'always'],
+ 'sort-vars': 1,
+ 'space-before-blocks': 1,
+ 'space-before-function-paren': [1, { anonymous: 'never', named: 'never', asyncArrow: 'always' }],
+ 'space-in-parens': 1,
+ 'space-infix-ops': 1,
+ 'space-unary-ops': 1,
+ 'spaced-comment': 1,
+ 'wrap-regex': 1,
+ '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..53b165d
--- /dev/null
+++ b/index.js
@@ -0,0 +1,112 @@
+const fs = require('node:fs');
+const chokidar = require('chokidar');
+const isLocalIP = require('./utils/isLocalIP.js');
+const { loadReportedIps, saveReportedIps, isIpReportedRecently, markIpAsReported } = require('./utils/cache.js');
+const log = require('./utils/log.js');
+const axios = require('./services/axios.js');
+const config = require('./config.js');
+const { LOG_FILE, ABUSEIPDB_API_KEY } = config.MAIN;
+
+let fileOffset = 0;
+
+const reportToAbuseIpDb = async (ip, categories, comment) => {
+ try {
+ const { data } = await axios.post('https://api.abuseipdb.com/api/v2/report', new URLSearchParams({ ip, categories, comment }), {
+ headers: { 'Key': ABUSEIPDB_API_KEY },
+ });
+
+ log(0, `Successfully reported IP ${ip} (score: ${data.data.abuseConfidenceScore})`);
+ return true;
+ } catch (err) {
+ log(2, `${err.message}\n${JSON.stringify(err.response.data)}`);
+ return false;
+ }
+};
+
+const determineCategories = (proto, dpt) => {
+ const categories = {
+ TCP: {
+ 22: '14,22,18', 80: '14,21', 443: '14,21', 8080: '14,21',
+ 25: '14,11', 21: '14,5,18', 53: '14,1,2', 23: '14,15,18',
+ 3389: '14,15,18', 3306: '14,16', 6666: '14,8',
+ 6667: '14,8', 6668: '14,8', 6669: '14,8', 9999: '14,6',
+ },
+ UDP: {
+ 53: '14,1,2', 123: '14,17',
+ },
+ };
+
+ return categories[proto]?.[dpt] || '14';
+};
+
+const processLogLine = async line => {
+ if (!line.includes('[UFW BLOCK]')) return log(1, `Ignoring line: ${line}`);
+
+ const match = {
+ timestamp: line.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[+-]\d{2}:\d{2})?/)[0],
+ srcIp: line.match(/SRC=([\d.]+)/)?.[1],
+ dstIp: line.match(/DST=([\d.]+)/)?.[1],
+ proto: line.match(/PROTO=(\S+)/)?.[1],
+ spt: line.match(/SPT=(\d+)/)?.[1],
+ dpt: line.match(/DPT=(\d+)/)?.[1],
+ ttl: line.match(/TTL=(\d+)/)?.[1],
+ len: line.match(/LEN=(\d+)/)?.[1],
+ tos: line.match(/TOS=(\S+)/)?.[1],
+ };
+
+ const { srcIp, proto, dpt } = match;
+ if (!srcIp) {
+ log(1, `Missing SRC in log line: ${line}`);
+ return;
+ }
+
+ if (isLocalIP(srcIp)) {
+ log(0, `Ignoring local/private IP: ${srcIp}`);
+ return;
+ }
+
+ if (isIpReportedRecently(srcIp)) {
+ log(0, `IP ${srcIp} reported recently`);
+ return;
+ }
+
+ const categories = determineCategories(proto, dpt);
+ const comment = config.REPORT_COMMENT(match.timestamp, srcIp, match.dstIp, proto, match.spt, dpt, match.ttl, match.len, match.tos);
+
+ log(0, `Reporting IP ${srcIp} (${proto} ${dpt}) with categories ${categories}`);
+
+ if (await reportToAbuseIpDb(srcIp, categories, comment)) {
+ markIpAsReported(srcIp);
+ saveReportedIps();
+ }
+};
+
+const startMonitoring = () => {
+ loadReportedIps();
+
+ if (!fs.existsSync(LOG_FILE)) {
+ log(2, `Log file ${LOG_FILE} does not exist.`);
+ return;
+ }
+
+ fileOffset = fs.statSync(LOG_FILE).size;
+
+ chokidar.watch(LOG_FILE, { persistent: true, ignoreInitial: true })
+ .on('change', path => {
+ const stats = fs.statSync(path);
+ if (stats.size < fileOffset) {
+ log(1, 'File truncated. Resetting offset...');
+ fileOffset = 0;
+ }
+
+ fs.createReadStream(path, { start: fileOffset, encoding: 'utf8' }).on('data', chunk => {
+ chunk.split('\n').filter(line => line.trim()).forEach(processLogLine);
+ }).on('end', () => {
+ fileOffset = stats.size;
+ });
+ });
+
+ log(0, `Now monitoring ${LOG_FILE}`);
+};
+
+startMonitoring();
\ No newline at end of file
diff --git a/install.sh b/install.sh
deleted file mode 100644
index 1f71044..0000000
--- a/install.sh
+++ /dev/null
@@ -1,261 +0,0 @@
-#!/bin/bash
-
-###
-# https://github.com/sefinek/UFW-AbuseIPDB-Reporter
-##
-
-VERSION="1.1.1"
-DATE="01.12.2024"
-REPO="https://github.com/sefinek/UFW-AbuseIPDB-Reporter"
-
-cat << "EOF"
- _ _ ___ ____ ____ ____
- / \ | |__ _ _ ___ ___ |_ _| | _ \ | _ \ | __ )
- / _ \ | '_ \ | | | | / __| / _ \ | | | |_) | | | | | | _ \
- / ___ \ | |_) | | |_| | \__ \ | __/ | | | __/ | |_| | | |_) |
- /_/ \_\_|_.__/ _ \__,_| |___/ \___| |___| |_| |____/ |____/
-
- (_)_ __ | |_ ___ __ _ _ __ __ _| |_(_) ___ _ __
- | | '_ \| __/ _ \/ _` | '__/ _` | __| |/ _ \| '_ \
- | | | | | || __/ (_| | | | (_| | |_| | (_) | | | |
- |_|_| |_|\__\___|\__, |_| \__,_|\__|_|\___/|_| |_|
- |___/
-
-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
-
- 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;;
- esac
- done
-}
-
-# ========================= CHECK FOR MISSING PACKAGES =========================
-required_packages=(ufw jq openssl)
-missing_packages=()
-
-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
-
-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
- else
- echo -e "INFO: Existing service will not be removed\n"
- fi
-fi
-
-
-# =========================== 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."
- exit 1
- fi
-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
- break
- fi
-
- 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."
- fi
-done
-
-if [[ "$valid_token" != true ]]; then
- echo "FAIL: Maximum number of attempts reached. Installation aborted!"
- exit 1
-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
-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
-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
-else
- echo "INFO: abuseipdb-ufw.service will not be added to autostart. Installation finished!"
-fi
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..34e9794
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,181 @@
+{
+ "name": "ufw-abuseipdb-reporter",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "ufw-abuseipdb-reporter",
+ "version": "1.0.0",
+ "license": "GPL-3.0",
+ "dependencies": {
+ "axios": "^1.7.9",
+ "chokidar": "^4.0.3",
+ "ipaddr.js": "^2.2.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.17.0",
+ "globals": "^15.14.0"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz",
+ "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "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/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/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/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.0.2",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz",
+ "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..ffb01ae
--- /dev/null
+++ b/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "ufw-abuseipdb-reporter",
+ "version": "1.0.0",
+ "description": "",
+ "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"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.17.0",
+ "globals": "^15.14.0"
+ },
+ "dependencies": {
+ "axios": "^1.7.9",
+ "chokidar": "^4.0.3",
+ "ipaddr.js": "^2.2.0"
+ }
+}
diff --git a/reporter.sh b/reporter.sh
deleted file mode 100644
index db67649..0000000
--- a/reporter.sh
+++ /dev/null
@@ -1,214 +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]+')
-
- 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..f33a5ab
--- /dev/null
+++ b/services/axios.js
@@ -0,0 +1,13 @@
+const axios = require('axios');
+const { version, homepage } = require('../config.js');
+
+axios.defaults.headers.common = {
+ 'User-Agent': `Mozilla/5.0 (compatible; UFW-AbuseIPDB-Reporter/${version}; +${homepage})`,
+ '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/utils/cache.js b/utils/cache.js
new file mode 100644
index 0000000..50b2b64
--- /dev/null
+++ b/utils/cache.js
@@ -0,0 +1,30 @@
+const fs = require('node:fs');
+const { CACHE_FILE, REPORT_INTERVAL } = require('../config.js').MAIN;
+const log = require('./log.js');
+
+const reportedIps = new Map();
+
+const loadReportedIps = () => {
+ if (fs.existsSync(CACHE_FILE)) {
+ fs.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 = () => fs.writeFileSync(CACHE_FILE, Array.from(reportedIps).map(([ip, time]) => `${ip} ${time}`).join('\n'), 'utf8');
+
+const isIpReportedRecently = ip => {
+ const now = Math.floor(Date.now() / 1000);
+ return reportedIps.has(ip) && (now - reportedIps.get(ip) < REPORT_INTERVAL);
+};
+
+const markIpAsReported = ip => reportedIps.set(ip, Math.floor(Date.now() / 1000));
+
+module.exports = { loadReportedIps, saveReportedIps, isIpReportedRecently, markIpAsReported };
\ 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