Node.js version (not finished yet)

This commit is contained in:
Sefinek 2024-12-19 14:58:18 +01:00
parent 4469aa0816
commit c19d2dd7c1
18 changed files with 487 additions and 562 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
node_modules
config.js

View file

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

6
.idea/jsLibraryMappings.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<includedPredefinedLibrary name="Node.js Core" />
</component>
</project>

7
.idea/jsLinters/eslint.xml generated Normal file
View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EslintConfiguration">
<custom-configuration-file used="true" path="$PROJECT_DIR$/eslint.config.mjs" />
<option name="fix-on-save" value="true" />
</component>
</project>

View file

@ -1,8 +1,3 @@
<div align="center">
[<a href="README.md">English</a>]
[<a href="README_PL.md">Polski</a>]
</div>
# 🛡️ 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.

View file

@ -1,82 +0,0 @@
<div align="center">
[<a href="README.md">English</a>]
[<a href="README_PL.md">Polski</a>]
</div>
# 🛡️ 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.

20
default.config.js Normal file
View file

@ -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. 💙
};

1
ecosystem.config.js Normal file
View file

@ -0,0 +1 @@
module.exports = {};

55
eslint.config.mjs Normal file
View file

@ -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/*'],
},
];

112
index.js Normal file
View file

@ -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();

View file

@ -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 <<EOF
>> 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

181
package-lock.json generated Normal file
View file

@ -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/"
}
}
}
}

34
package.json Normal file
View file

@ -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 <contact@sefinek.net> (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"
}
}

View file

@ -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

13
services/axios.js Normal file
View file

@ -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;

30
utils/cache.js Normal file
View file

@ -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 };

10
utils/isLocalIP.js Normal file
View file

@ -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);
};

10
utils/log.js Normal file
View file

@ -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}`);
};