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 @@ -
- [English] - [Polski] -
- # 🛡️ 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 @@ -
- [English] - [Polski] -
- -# 🛡️ 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