const fs = require('node:fs'); const chokidar = require('chokidar'); const isLocalIP = require('./utils/isLocalIP.js'); const { reportedIps, 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} (abuse: ${data.data.abuseConfidenceScore}%)`); return true; } catch (err) { log(2, `${err.message}\n${JSON.stringify(err.response.data)}`); return false; } }; 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)) { const lastReportedTime = reportedIps.get(srcIp); const elapsedTime = Math.floor(Date.now() / 1000 - lastReportedTime); const days = Math.floor(elapsedTime / 86400); const hours = Math.floor((elapsedTime % 86400) / 3600); const minutes = Math.floor((elapsedTime % 3600) / 60); const seconds = elapsedTime % 60; const timeAgo = [ days && `${days}d`, hours && `${hours}h`, minutes && `${minutes}m`, (seconds || !days && !hours && !minutes) && `${seconds}s`, ].filter(Boolean).join(' '); log(0, `IP ${srcIp} was last reported on ${new Date(lastReportedTime * 1000).toLocaleString()} (${timeAgo} ago)`); return; } const categories = config.DETERMINE_CATEGORIES(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();