Unlink scripts submodule and commit contents as normal directory

This commit is contained in:
Kyle Speight committed 2026-06-14 03:42:18 -07:00
1 parent 4f7dea111f
commit 6488b3ea85
48 files changed
+2876 -1

No files matched your search

Submodule scripts deleted from 190e59627e.
+53
View File
@@ -0,0 +1,53 @@
SniffCat:
1 - DNS Compromise: Altering DNS records resulting in improper redirection to malicious IPs.
2 - DNS Poisoning: Falsifying domain server cache (cache poisoning) with fake DNS responses.
3 - DDoS Attack: Participating in distributed denial-of-service attack causing service unavailability.
4 - Port Scan: Scanning TCP/UDP ports to discover open services or vulnerabilities.
5 - Mass Scanner: Automated scanning of multiple hosts or entire network subnets.
6 - Exploited Host: Host compromised and controlled by attacker, actively used for malicious activities.
7 - Malware Hosting: Server distributing malicious software, scripts, or infected files.
8 - C&C Beaconing: Communication with malware command-and-control (C2) infrastructure.
9 - Cryptojacking: Unauthorized use of victim's system resources for cryptocurrency mining.
10 - Phishing: Impersonating legitimate websites to steal credentials or personal data.
11 - Hacking: General hacking attempts to gain unauthorized access or escalate privileges.
12 - SQL Injection: Injecting malicious SQL queries through user input fields.
13 - Command Injection: Executing arbitrary system commands through vulnerable application backend.
14 - Spam Activity: Automated generation of spam (forms, comments, messages, emails).
15 - Bad Web Bot: Malicious bot ignoring robots.txt or impersonating legitimate user-agents.
16 - Path Traversal: Accessing system files outside application directory using ../ techniques.
17 - Brute-Force: Mass login attempts using automated credential guessing.
18 - SSH/SFTP: Unauthorized login attempts to SSH or SFTP services.
19 - FTP: Unauthorized login attempts to FTP server.
20 - Email (SMTP/IMAP/POP3): Unauthorized access attempts to email services.
21 - HTTP/HTTPS: Suspicious HTTP/S requests with malicious headers or payloads.
22 - RDP: Unauthorized login attempts to Remote Desktop Protocol.
23 - Telnet: Unauthorized access to Telnet service, commonly on IoT devices.
24 - SMB: Unauthorized login attempts to SMB/CIFS file shares.
25 - MongoDB: Unauthorized access attempts to MongoDB database instances.
26 - Redis: Unauthorized login attempts to open Redis instances.
27 - Other Abuse: Security-violating activity not covered by other categories.
AbuseIPDB:
1 - DNS Compromise: Altering DNS records resulting in improper redirection to malicious IPs.
2 - DNS Poisoning: Falsifying domain server cache (cache poisoning) with fake DNS responses.
3 - Fraud Orders: Fraudulent orders placed using stolen payment information or fake identities.
4 - DDoS Attack: Participating in distributed denial-of-service attack, usually as part of a botnet.
5 - FTP Brute-Force: Automated credential guessing attacks against FTP services.
6 - Ping of Death: Sending oversized or malformed ICMP packets to crash or destabilize systems.
7 - Phishing: Impersonating legitimate websites or sending deceptive emails to steal credentials.
8 - Fraud VoIP: Fraudulent activity involving Voice over IP services or toll fraud.
9 - Open Proxy: Operating as open proxy, open relay, or Tor exit node enabling anonymous abuse.
10 - Web Spam: Comment/forum spam, HTTP referer spam, or other CMS-based spam content.
11 - Email Spam: Spam email content, infected attachments, and phishing emails.
12 - Blog Spam: CMS blog comment spam targeting WordPress, Drupal, or similar platforms.
13 - VPN IP: Conjunctive category for traffic originating from VPN or anonymizing services.
14 - Port Scan: Scanning for open ports and vulnerable services across network ranges.
15 - Hacking: General hacking attempts to gain unauthorized access or escalate privileges.
16 - SQL Injection: Injecting malicious SQL queries through user input fields to access databases.
17 - Spoofing: Email sender spoofing or IP address spoofing to impersonate trusted sources.
18 - Brute-Force: Credential brute-force attacks on logins and services like SSH, FTP, SIP, SMTP, RDP.
19 - Bad Web Bot: Malicious bot ignoring robots.txt, scraping content, or spoofing user-agents.
20 - Exploited Host: Host compromised with malware, used for attacks or hosting malicious content.
21 - Web App Attack: Probing or exploiting web applications like WordPress, phpMyAdmin, or e-commerce.
22 - SSH: Unauthorized login attempts or abuse targeting Secure Shell (SSH) services.
23 - IoT Targeted: Abuse targeting Internet of Things devices like routers, cameras, or smart devices.
+33
View File
@@ -0,0 +1,33 @@
# 🔧 Integration Scripts
## Supported Services
- [SniffCat](https://sniffcat.com)
- [AbuseIPDB](https://www.abuseipdb.com/user/158699)
## Used for
| | [SniffCat](https://sniffcat.com/users/681e779a3e78a7a67816b8a2) | [AbuseIPDB](https://www.abuseipdb.com/user/158699) |
|------------|----------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------|
| UFW | [UFW-SniffCat-Reporter](https://github.com/SniffCatDB/UFW-SniffCat-Reporter) | [UFW-AbuseIPDB-Reporter](https://github.com/sefinek/UFW-AbuseIPDB-Reporter) |
| Cloudflare | [Cloudflare-WAF-To-SniffCat](https://github.com/SniffCatDB/Cloudflare-WAF-To-SniffCat) | [Cloudflare-WAF-To-AbuseIPDB](https://github.com/sefinek/Cloudflare-WAF-To-AbuseIPDB) |
| T-Pot | [T-Pot-To-SniffCat](https://github.com/SniffCatDB/T-Pot-To-SniffCat) | [T-Pot-To-AbuseIPDB](https://github.com/sefinek/T-Pot-To-AbuseIPDB) |
| Suricata | Soon | [Suricata-To-AbuseIPDB](https://github.com/sefinek/Suricata-To-AbuseIPDB) |
## Terms
| Key | Description |
|-------------|------------------------------------------------------|
| `timestamp` | Event timestamp (UTC format) |
| `srcIp` | Source IP address (attacker) |
| `dstIp` | Destination IP address (victim/server) |
| `proto` | Protocol used (e.g., TCP, SSH, TELNET, MONGOD) |
| `spt` | Source port number |
| `dpt` | Destination port number |
| `in` | Incoming network interface name (e.g., enp1s0, ens3) |
| `out` | Outgoing network interface name |
| `mac` | MAC address information |
| `len` | Total packet length (in bytes) |
| `ttl` | Packet TTL (Time To Live) |
| `id` | Packet ID |
| `tos` | Type of Service field in IP header |
| `prec` | Precedence value in TOS field |
| `window` | TCP window size |
| `urgp` | TCP urgent pointer |
+14
View File
@@ -0,0 +1,14 @@
const { version, authorMeta, prettyName, license } = require('../repo.js');
const { color, reset, headerText, authorText, donate, sniffcat } = require('./utils/helper.js');
const logger = require('../logger.js');
module.exports = () => {
console.log(`
${color('255;160;0')} .--. ${headerText(`Cloudflare WAF To ${prettyName} - v${version} `)}
${color('255;140;0')}.-( ). ${authorText(authorMeta)} [${license}]
${color('255;120;0')}(___.__)__)${reset}
`);
logger.info(donate);
logger.info(sniffcat);
};
+16
View File
@@ -0,0 +1,16 @@
const { version, authorMeta, prettyName, repoUrl, license } = require('../repo.js');
const { color, reset, headerText, authorText, donate } = require('./utils/helper.js');
const logger = require('../logger.js');
module.exports = () => {
console.log(`
${color('255;140;0')} /\\_/\\
${color('255;120;0')} ( o.o ) ${headerText(`Suricata To ${prettyName} - v${version}`)}
${color('255;100;0')} > ^ < ${authorText(authorMeta)}
${color('255;80;0')} / \\ ${authorText(`${repoUrl} [${license}]`)}
${color('255;70;0')}( )
${color('255;40;0')} \\__|__/${reset}
`);
logger.info(donate);
};
+16
View File
@@ -0,0 +1,16 @@
const { version, authorMeta, prettyName, repoUrl, license } = require('../repo.js');
const { color, reset, donate, sniffcat } = require('./utils/helper.js');
const logger = require('../logger.js');
module.exports = () => {
console.log(`
${color('255;0;130')} _____ ____ _
${color('235;0;120')}|_ _| | _ \\ ___ | |_ ${color('235;0;120')}T${color('235;0;120')}-${color('235;0;120')}Pot To ${prettyName} - v${version}
${color('215;0;110')} | | | |_) / _ \\| __| ${color('235;0;120')}${authorMeta}
${color('195;0;95')} | | | __/ (_) | |_ ${repoUrl} [${license}]
${color('175;0;85')} |_| |_| \\___/ \\__|${reset}
`);
logger.info(donate);
logger.info(sniffcat);
};
+17
View File
@@ -0,0 +1,17 @@
const { version, authorMeta, prettyName, repoUrl, license } = require('../repo.js');
const { color, reset, headerText, authorText, donate, sniffcat } = require('./utils/helper.js');
const logger = require('../logger.js');
module.exports = () => {
console.log(`
${color('255;140;0')}██╗ ██╗ ███████╗ ██╗ ██╗
${color('255;120;0')}██║ ██║ ██╔════╝ ██║ ██║ ${headerText(`UFW To ${prettyName} - v${version}`)}
${color('255;100;0')}██║ ██║ █████╗ ██║ █╗ ██║ ${authorText(authorMeta)}
${color('255;80;0')}██║ ██║ ██╔══╝ ██║███╗██║ ${repoUrl} ${authorText(`[${license}]`)}
${color('255;70;0')}╚██████╔╝ ██║ ╚███╔███╔╝
${color('255;40;0')} ╚═════╝ ╚═╝ ╚══╝╚══╝${reset}
`);
logger.info(donate);
logger.info(sniffcat);
};
+7
View File
@@ -0,0 +1,7 @@
module.exports = (text, startGreen, endGreen) => {
const textLength = text.length;
return [...text].map((char, index) => {
const greenValue = Math.round(startGreen - ((startGreen - endGreen) * index / (textLength - 1)));
return `\x1b[38;2;255;${greenValue};0m${char}`;
}).join('');
};
+19
View File
@@ -0,0 +1,19 @@
const gradientText = require('./gradientText.js');
const IS_PM2 = 'pm_id' in process.env;
const color = code => IS_PM2 ? '' : `\x1b[38;2;${code}m`;
const reset = IS_PM2 ? '' : '\x1b[0m';
const headerText = text => IS_PM2 ? text : gradientText(text, 145, 58);
const authorText = text => IS_PM2 ? text : gradientText(text, 145, 88);
module.exports = Object.freeze({
IS_PM2,
color,
reset,
headerText,
authorText,
donate: '💗 If you find this project useful, consider supporting me: https://sefinek.net/donate',
sniffcat: '📘 Consider reporting malicious addresses to SniffCat, a modern alternative to AbuseIPDB: https://sniffcat.com',
});
+35
View File
@@ -0,0 +1,35 @@
const fs = require('node:fs');
const path = require('node:path');
if (process.argv.includes('--help')) {
const readme = path.join(__dirname, '..', 'README.md');
if (!fs.existsSync(readme)) {
console.log('No README.md found.\n');
process.exit(1);
}
const lines = fs.readFileSync(readme, 'utf8').split('\n');
const start = lines.findIndex(l => l.startsWith('| `--'));
if (start === -1) {
console.log('No valid CLI options found in README.md.\n');
process.exit(1);
}
const options = [];
for (let i = start; i < lines.length && lines[i].startsWith('|'); i++) {
const cols = lines[i].split('|').map(v => v.trim());
if (cols.length < 3) continue;
const name = cols[1].replace(/`/g, '');
const desc = cols[2].replace(/\s+/g, ' ');
if (name && desc) options.push([name, desc]);
}
const pad = Math.max(...options.map(([n]) => n.length)) + 2;
console.log('\nUsage: node . [options]\n\nOptions:');
for (const [name, desc] of options) {
console.log(` ${name.padEnd(pad)}${desc}`);
}
console.log('\nExample: node . --run-on-start\n');
process.exit(0);
}
+200
View File
@@ -0,0 +1,200 @@
const { prettyName } = require('./repo.js');
const logger = require('./logger.js');
/**
* @typedef {
* | 'DNS_COMPROMISE'
* | 'DNS_POISONING'
* | 'DDOS_ATTACK'
* | 'PORT_SCAN'
* | 'MASS_SCANNER'
* | 'EXPLOITED_HOST'
* | 'MALWARE_HOSTING'
* | 'CNC_BEACONING'
* | 'CRYPTOJACKING'
* | 'PHISHING'
* | 'HACKING'
* | 'SQL_INJECTION'
* | 'COMMAND_INJECTION'
* | 'SPAM_ACTIVITY'
* | 'BAD_WEB_BOT'
* | 'PATH_TRAVERSAL'
* | 'BRUTE_FORCE'
* | 'SSH'
* | 'FTP'
* | 'EMAIL'
* | 'HTTP'
* | 'RDP'
* | 'TELNET'
* | 'SMB'
* | 'MONGODB'
* | 'REDIS'
* | 'OTHER_ABUSE'
* | 'FRAUD_ORDERS'
* | 'PING_OF_DEATH'
* | 'FRAUD_VOIP'
* | 'OPEN_PROXY'
* | 'WEB_SPAM'
* | 'EMAIL_SPAM'
* | 'BLOG_SPAM'
* | 'VPN_IP'
* | 'SPOOFING'
* | 'WEB_APP_ATTACK'
* | 'IOT_TARGETED'
* | 'IGNORING_ROBOTS_TXT'
* } Flag
*/
/** @type {Record<Flag, Flag>} */
const FLAGS = Object.freeze({
DNS_COMPROMISE: 'DNS_COMPROMISE',
DNS_POISONING: 'DNS_POISONING',
DDOS_ATTACK: 'DDOS_ATTACK',
PORT_SCAN: 'PORT_SCAN',
MASS_SCANNER: 'MASS_SCANNER',
EXPLOITED_HOST: 'EXPLOITED_HOST',
MALWARE_HOSTING: 'MALWARE_HOSTING',
CNC_BEACONING: 'CNC_BEACONING',
CRYPTOJACKING: 'CRYPTOJACKING',
PHISHING: 'PHISHING',
HACKING: 'HACKING',
SQL_INJECTION: 'SQL_INJECTION',
COMMAND_INJECTION: 'COMMAND_INJECTION',
SPAM_ACTIVITY: 'SPAM_ACTIVITY',
BAD_WEB_BOT: 'BAD_WEB_BOT',
PATH_TRAVERSAL: 'PATH_TRAVERSAL',
BRUTE_FORCE: 'BRUTE_FORCE',
SSH: 'SSH',
FTP: 'FTP',
EMAIL: 'EMAIL',
HTTP: 'HTTP',
RDP: 'RDP',
TELNET: 'TELNET',
SMB: 'SMB',
MONGODB: 'MONGODB',
REDIS: 'REDIS',
OTHER_ABUSE: 'OTHER_ABUSE',
FRAUD_ORDERS: 'FRAUD_ORDERS',
PING_OF_DEATH: 'PING_OF_DEATH',
FRAUD_VOIP: 'FRAUD_VOIP',
OPEN_PROXY: 'OPEN_PROXY',
WEB_SPAM: 'WEB_SPAM',
EMAIL_SPAM: 'EMAIL_SPAM',
BLOG_SPAM: 'BLOG_SPAM',
VPN_IP: 'VPN_IP',
SPOOFING: 'SPOOFING',
WEB_APP_ATTACK: 'WEB_APP_ATTACK',
IOT_TARGETED: 'IOT_TARGETED',
IGNORING_ROBOTS_TXT: 'IGNORING_ROBOTS_TXT',
});
const MAPPINGS = Object.freeze({
SniffCat: new Map([
['DNS_COMPROMISE', 1],
['DNS_POISONING', 2],
['DDOS_ATTACK', 3],
['PORT_SCAN', 4],
['MASS_SCANNER', 5],
['EXPLOITED_HOST', 6],
['MALWARE_HOSTING', 7],
['CNC_BEACONING', 8],
['CRYPTOJACKING', 9],
['PHISHING', 10],
['HACKING', 11],
['SQL_INJECTION', 12],
['COMMAND_INJECTION', 13],
['SPAM_ACTIVITY', 14],
['BAD_WEB_BOT', 15],
['PATH_TRAVERSAL', 16],
['BRUTE_FORCE', 17],
['SSH', 18],
['FTP', 19],
['EMAIL', 20],
['HTTP', 21],
['RDP', 22],
['TELNET', 23],
['SMB', 24],
['MONGODB', 25],
['REDIS', 26],
['OTHER_ABUSE', 27],
]),
AbuseIPDB: new Map([
['DNS_COMPROMISE', 1],
['DNS_POISONING', 2],
['FRAUD_ORDERS', 3],
['DDOS_ATTACK', 4],
['FTP', 5],
['PING_OF_DEATH', 6],
['PHISHING', 7],
['FRAUD_VOIP', 8],
['OPEN_PROXY', 9],
['WEB_SPAM', 10],
['EMAIL_SPAM', 11],
['BLOG_SPAM', 12],
['VPN_IP', 13],
['PORT_SCAN', 14],
['HACKING', 15],
['SQL_INJECTION', 16],
['SPOOFING', 17],
['BRUTE_FORCE', 18],
['BAD_WEB_BOT', 19],
['EXPLOITED_HOST', 20],
['WEB_APP_ATTACK', 21],
['SSH', 22],
['IOT_TARGETED', 23],
]),
});
const FALLBACK_IDS = {
SniffCat: MAPPINGS.SniffCat.get(FLAGS.OTHER_ABUSE),
AbuseIPDB: MAPPINGS.AbuseIPDB.get(FLAGS.HACKING),
};
const createFlagCollection = () => {
const flags = new Set();
const list = () => Array.from(flags);
const toIDs = (integration = prettyName) => {
const map = MAPPINGS[integration];
if (!map) throw new Error(`Unsupported integration: ${integration}`);
const fallbackID = FALLBACK_IDS[integration];
if (typeof fallbackID !== 'number') {
throw new Error(`No fallback ID defined for integration: ${integration}`);
}
const currentFlags = list();
if (currentFlags.length === 0) return [fallbackID];
const ids = currentFlags
.map(f => map.get(f) ?? fallbackID)
.filter(id => typeof id === 'number');
return ids.length > 0 ? ids : [fallbackID];
};
const api = {
add: /** @param {...Flag} items */ (...items) => {
for (const item of items) {
if (Object.prototype.hasOwnProperty.call(FLAGS, item)) {
flags.add(item);
} else {
logger.warn(`Invalid abuse flag ignored! flag='${item}' integration='${prettyName}'`, { discord: true, ping: false });
}
}
return api;
},
list,
toIDs,
toString: (integration = prettyName) => toIDs(integration).join(','),
};
return api;
};
module.exports = {
/** @type {Record<Flag, Flag>} */
FLAGS,
createFlagCollection,
};
+32
View File
@@ -0,0 +1,32 @@
const { createFlagCollection } = require('./flags.js');
const flags = createFlagCollection();
// No flags added — expect default fallbacks:
console.log(flags.toIDs('SniffCat')); // [27]
console.log(flags.toIDs('AbuseIPDB')); // [15]
// console.log(flags.toIDs('UnknownSystem'));
// Add some flags that are mapped in both systems:
flags.add('DDOS_ATTACK', 'PHISHING', 'PORT_SCAN');
console.log(flags.list()); // ['DDOS_ATTACK', 'PHISHING', 'PORT_SCAN']
console.log(flags.toIDs('SniffCat')); // [3, 10, 4]
console.log(flags.toIDs('AbuseIPDB')); // [4, 7, 14]
console.log(flags.toString('AbuseIPDB')); // "4,7,14"
// Add a flag that doesn't exist in AbuseIPDB:
flags.add('EMAIL'); // Present in SniffCat only
console.log(flags.toIDs('SniffCat')); // [3, 10, 4, 20]
console.log(flags.toIDs('AbuseIPDB')); // [4, 7, 14, 15] ← fallback 15 for 'EMAIL'
// Add a flag that only exists in AbuseIPDB:
flags.add('FRAUD_VOIP'); // Present in AbuseIPDB only
console.log(flags.toIDs('AbuseIPDB')); // [4, 7, 14, 15, 8]
console.log(flags.toIDs('SniffCat')); // [3, 10, 4, 20, 27] ← fallback 27 for 'FRAUD_VOIP'
// Unknown integration
// console.log(flags.toIDs('UnknownSystem')); // []
+10
View File
@@ -0,0 +1,10 @@
module.exports = iso => {
try {
const date = new Date(iso);
if (isNaN(date.getTime())) throw new Error('Invalid Date');
return date.toISOString().split('.')[0] + 'Z';
} catch {
return new Date().toISOString().split('.')[0] + 'Z';
}
};
+271
View File
@@ -0,0 +1,271 @@
const { FLAGS, createFlagCollection } = require('../flags.js');
const logIpToFile = require('../logIpToFile.js');
const tailFile = require('../services/tailFile.js');
const logger = require('../logger.js');
const resolvePath = require('../pathResolver.js');
const { COWRIE_LOG_FILE, SERVER_ID } = require('../../config.js').MAIN;
const LOG_FILE = resolvePath(COWRIE_LOG_FILE);
const REPORT_DELAY = SERVER_ID === 'development' ? 30 * 1000 : 10 * 60 * 1000;
const CREDS_LIMIT = 885;
const ipBuffers = new Map();
const extractSessionData = sessions => {
const commands = [];
const flags = createFlagCollection();
const credsSet = new Set();
const fingerprints = new Set();
const uploads = new Set();
const tunnels = new Set();
let dpt = null, proto = null, sshVersion = null, timestamp = null;
const downloadUrls = new Set();
for (const s of sessions) {
dpt ??= s.dpt;
proto ??= s.proto;
sshVersion ??= s.sshVersion;
timestamp ??= s.timestamp;
s.credentials?.forEach((_, cred) => credsSet.add(cred));
commands.push(...s.commands);
if (s.fingerprint) fingerprints.add(s.fingerprint);
if (s.uploads) s.uploads.forEach(f => uploads.add(f));
if (s.tunnels) s.tunnels.forEach(t => tunnels.add(t));
if (s.download?.url) {
flags.add(FLAGS.WEB_APP_ATTACK);
downloadUrls.add(s.download.url);
}
}
const creds = [...credsSet];
const loginAttempts = creds.length;
const cmdCount = commands.length;
if (loginAttempts >= 2) flags.add(FLAGS.BRUTE_FORCE);
if (proto === 'ssh') flags.add(FLAGS.SSH);
if (proto === 'telnet') flags.add(FLAGS.TELNET);
if (cmdCount) flags.add(FLAGS.EMAIL);
if (!loginAttempts && !cmdCount) flags.add(FLAGS.PORT_SCAN);
return {
dpt, proto, sshVersion, timestamp, creds, commands,
categories: flags.toString(),
downloadUrls: [...downloadUrls],
fingerprints: [...fingerprints],
uploads: [...uploads],
tunnels: [...tunnels],
};
};
const buildComment = ({ serverId, dpt, proto, creds, commands, sshVersion, downloadUrls, fingerprints, uploads, tunnels }, full = false) => {
const loginAttempts = creds.length;
const cmdCount = commands.length;
const lines = [];
lines.push(`Honeypot ${serverId ? `[${serverId}]` : 'hit'}: ${loginAttempts ? 'Brute-force attack' : 'Unauthorized connection attempt'} detected on ${dpt}/${proto.toUpperCase()}`);
if (loginAttempts === 1) {
lines.push(`• Credential used: ${creds[0]}`);
} else if (loginAttempts > 1) {
let joined = creds.join(', ');
if (!full && joined.length > CREDS_LIMIT) joined = joined.slice(0, CREDS_LIMIT).replace(/,[^,]*$/, '') + '...';
lines.push(`• Credentials: ${joined}`);
}
if (loginAttempts) lines.push(`• Number of login attempts: ${loginAttempts}`);
if (cmdCount) lines.push(`• ${cmdCount} command(s) were executed during the session`);
if (sshVersion) lines.push(`• Client: ${sshVersion}`);
if (downloadUrls.length) lines.push(`• Suspicious file URLs: ${downloadUrls.join(', ')}`);
if (fingerprints.length) lines.push(`• SSH key fingerprints: ${fingerprints.join(', ')}`);
if (uploads.length) lines.push(`• Uploaded files: ${uploads.join(', ')}`);
if (tunnels.length) lines.push(`• TCP tunnels: ${tunnels.join(', ')}`);
return lines.join('\n');
};
const flushBuffer = async (srcIp, reportIp) => {
const buffer = ipBuffers.get(srcIp);
if (!buffer) return;
clearTimeout(buffer.timer);
ipBuffers.delete(srcIp);
const sessions = buffer.sessions || [];
if (!sessions.length) return;
const {
dpt, proto, sshVersion, timestamp, creds, commands,
categories, downloadUrls, fingerprints, uploads, tunnels,
} = extractSessionData(sessions);
if (!srcIp || !dpt || !proto) {
return logger.warn(`COWRIE -> Incomplete data for ${srcIp}, discarded`);
}
const shortComment = buildComment({ serverId: SERVER_ID, dpt, proto, creds, commands, sshVersion, downloadUrls, fingerprints, uploads, tunnels }, false);
const [, ...restLines] = shortComment.split('\n');
const rest = restLines.length ? `\n${restLines.join('\n')}` : '';
await reportIp('COWRIE', { srcIp, dpt, proto, timestamp }, categories, shortComment);
if (restLines.length) await logger.info(rest);
await logger.webhook(`### Cowrie: ${srcIp} on ${dpt}/${proto}${rest}`);
logIpToFile(srcIp, { honeypot: 'COWRIE', comment: buildComment({ serverId: SERVER_ID, dpt, proto, creds, commands, sshVersion, downloadUrls, fingerprints, uploads, tunnels }, true) });
};
const processCowrieLogLine = async (entry, reportIp) => {
const ip = entry?.src_ip;
const sessionId = entry?.session;
const { eventid } = entry;
if (!ip || !eventid || !sessionId) return logger.warn('COWRIE -> Skipped: missing src_ip, eventid or sessionId');
let buffer = ipBuffers.get(ip);
if (!buffer) {
buffer = {
sessions: [],
timer: setTimeout(() => flushBuffer(ip, reportIp), REPORT_DELAY),
lastSeen: Date.now(),
};
ipBuffers.set(ip, buffer);
} else {
buffer.lastSeen = Date.now();
}
let session = buffer.sessions.find(s => s.sessionId === sessionId);
if (!session && eventid !== 'cowrie.session.closed') {
session = {
sessionId,
srcIp: ip,
dpt: null,
proto: null,
timestamp: null,
credentials: new Map(),
commands: [],
sshVersion: null,
download: null,
fingerprint: null,
uploads: [],
tunnels: [],
};
buffer.sessions.push(session);
}
switch (eventid) {
case 'cowrie.session.connect':
if (session) {
session.dpt = entry.dst_port;
session.proto = entry.protocol;
session.timestamp = entry.timestamp;
logger.info(`COWRIE -> ${ip}/${session.proto}/${session.dpt}: Connect`);
}
break;
case 'cowrie.login.success':
case 'cowrie.login.failed':
if (session && (entry.username || entry.password)) {
session.credentials.set(`${entry.username}:${entry.password}`, true);
const status = eventid === 'cowrie.login.success' ? 'Connected' : 'Failed login';
logger.info(`COWRIE -> ${ip}/${session.proto}/${session.dpt}: ${status} » ${entry.username}:${entry.password}`);
}
break;
case 'cowrie.client.version':
if (session) {
session.sshVersion = entry.version;
logger.info(`COWRIE -> ${ip}/${session.proto}/${session.dpt}: SSH version » ${entry.version}`);
}
break;
case 'cowrie.command.input':
if (session && entry.input) {
session.commands.push(entry.input);
logger.info(`COWRIE -> ${ip}/${session.proto}/${session.dpt}: $ ${entry.input}`);
}
break;
case 'cowrie.session.file_download':
if (session && entry.url) {
session.commands.push(`[download] ${entry.url}`);
session.download = { url: entry.url, outfile: entry.outfile };
logger.info(`COWRIE -> ${ip}/${session.proto}/${session.dpt}: File download » ${entry.url}`);
}
break;
case 'cowrie.client.fingerprint':
if (session && entry.fingerprint) {
session.fingerprint = entry.fingerprint;
logger.info(`COWRIE -> ${ip}/${session.proto}/${session.dpt}: SSH key fingerprint » ${entry.fingerprint}`);
}
break;
case 'cowrie.session.file_upload':
if (session && entry.filename) {
session.uploads.push(entry.filename);
logger.info(`COWRIE -> ${ip}/${session.proto}/${session.dpt}: File upload » ${entry.filename}`);
}
break;
case 'cowrie.direct-tcpip.request':
if (session && entry.dst_ip && entry.dst_port) {
const tunnel = `${entry.dst_ip}:${entry.dst_port}`;
session.tunnels.push(tunnel);
logger.info(`COWRIE -> ${ip}/${session.proto}/${session.dpt}: TCP tunnel request » ${tunnel}`);
}
break;
case 'cowrie.session.closed':
logger.info(`COWRIE -> ${ip}/${session?.proto ?? 'unknown'}/${session?.dpt ?? '-'}: Session ${sessionId} closed`);
break;
}
};
module.exports = reportIp => {
tailFile(LOG_FILE, async line => {
if (!line.length) return;
let entry;
try {
entry = JSON.parse(line);
} catch (err) {
return logger.error(`COWRIE -> JSON parse error: ${err.message}\nFaulty line: ${JSON.stringify(line)}`);
}
try {
await processCowrieLogLine(entry, reportIp);
} catch (err) {
logger.error(err);
}
}, { label: 'COWRIE' }).catch(err => logger.error(err));
// Clean buffer
const cleanupInterval = setInterval(() => {
const now = Date.now();
for (const [ip, buffer] of ipBuffers.entries()) {
if (now - buffer.lastSeen > 30 * 60 * 1000) {
clearTimeout(buffer.timer);
ipBuffers.delete(ip);
logger.warn(`COWRIE -> Cleaned up stale session buffer for ${ip}`);
}
}
}, 15 * 60 * 1000);
logger.success('🛡️ COWRIE » Watcher initialized');
return {
flush: async () => {
for (const ip of ipBuffers.keys()) {
await flushBuffer(ip, reportIp);
}
},
cleanup: () => {
clearInterval(cleanupInterval);
for (const buffer of ipBuffers.values()) {
clearTimeout(buffer.timer);
}
ipBuffers.clear();
},
};
};
+97
View File
@@ -0,0 +1,97 @@
const { FLAGS, createFlagCollection } = require('../flags.js');
const logIpToFile = require('../logIpToFile.js');
const tailFile = require('../services/tailFile.js');
const logger = require('../logger.js');
const resolvePath = require('../pathResolver.js');
const { DIONAEA_LOG_FILE, SERVER_ID } = require('../../config.js').MAIN;
const LOG_FILE = resolvePath(DIONAEA_LOG_FILE);
const getReportDetails = (entry, dpt) => {
const proto = entry?.connection?.protocol || 'unknown';
const timestamp = entry?.timestamp || new Date().toISOString();
const flags = createFlagCollection();
let comment;
switch (proto) {
case 'mssqld': {
const username = entry?.credentials?.username?.[0];
const password = entry?.credentials?.password?.[0];
if (username && !password) {
flags.add(FLAGS.BRUTE_FORCE);
comment = `MSSQL traffic (on ${dpt}) with username ${username} and empty password`;
} else if (username && password) {
flags.add(FLAGS.BRUTE_FORCE);
comment = `MSSQL traffic (on ${dpt}) with credentials ${username}:${password}`;
} else {
flags.add(FLAGS.PORT_SCAN);
comment = `MSSQL traffic (on ${dpt}) without login credentials`;
}
break;
}
case 'httpd':
flags.add(FLAGS.HTTP, FLAGS.BAD_WEB_BOT);
comment = `Incoming HTTP traffic on port ${dpt}`;
break;
case 'ftp':
flags.add(FLAGS.FTP, FLAGS.BRUTE_FORCE);
comment = `FTP brute-force or probing on port ${dpt}`;
break;
case 'smbd':
flags.add(FLAGS.SMB);
comment = `SMB traffic on port ${dpt}`;
break;
case 'mysql':
flags.add(FLAGS.BRUTE_FORCE);
comment = `MySQL brute-force or probing on port ${dpt}`;
break;
case 'tftp':
flags.add(FLAGS.EXPLOITED_HOST);
comment = `TFTP protocol traffic on ${dpt}`;
break;
case 'upnp':
case 'mqtt':
flags.add(FLAGS.IOT_TARGETED);
comment = `Unauthorized ${proto.toUpperCase()} traffic on ${dpt}`;
break;
default:
flags.add(FLAGS.PORT_SCAN);
comment = `Unauthorized traffic on ${dpt}/${proto}`;
}
return {
proto: proto.toUpperCase(),
comment: `Honeypot ${SERVER_ID ? `[${SERVER_ID}]` : 'hit'}: ${comment}`,
categories: flags.toString(),
timestamp,
};
};
module.exports = reportIp => {
tailFile(LOG_FILE, async line => {
if (!line.length) return;
let entry;
try {
entry = JSON.parse(line);
} catch (err) {
return logger.error(`DIONAEA -> JSON parse error: ${err.message}\nFaulty line: ${JSON.stringify(line)}`);
}
try {
const srcIp = entry?.src_ip;
const dpt = entry?.dst_port;
if (!srcIp || !dpt) return;
const { proto, timestamp, categories, comment } = getReportDetails(entry, dpt);
await reportIp('DIONAEA', { srcIp, dpt, proto, timestamp }, categories, comment);
await logIpToFile(srcIp, { honeypot: 'DIONAEA', comment });
} catch (err) {
logger.error(err);
}
}, { label: 'DIONAEA' }).catch(err => logger.error(err));
logger.success('🛡️ DIONAEA » Watcher initialized');
};
+179
View File
@@ -0,0 +1,179 @@
const logIpToFile = require('../logIpToFile.js');
const tailFile = require('../services/tailFile.js');
const logger = require('../logger.js');
const resolvePath = require('../pathResolver.js');
const { HONEYTRAP_LOG_FILE, SERVER_ID } = require('../../config.js').MAIN;
const { FLAGS, createFlagCollection } = require('../flags.js');
const LOG_FILE = resolvePath(HONEYTRAP_LOG_FILE);
const HEADER_PRIORITY = ['user-agent', 'accept', 'accept-language', 'accept-encoding'];
let lastFlushTime = Date.now();
const attackBuffer = new Map();
const capitalizeHeader = header => header.split('-').map(w => w[0].toUpperCase() + w.slice(1)).join('-');
const parseHttpRequest = (ascii, dpt) => {
const lines = ascii.replace(/\r\n|\r/g, '\n').trim().split('\n');
const requestLineRaw = lines.shift()?.trim() || '';
const protocol = requestLineRaw.match(/HTTP\/[0-9.]+/i)?.[0]?.toUpperCase() || 'HTTP';
const requestLine = requestLineRaw.replace(/\s*HTTP\/[0-9.]+$/i, '');
const headers = {};
const body = [];
let parsingBody = false;
for (const line of lines) {
if (parsingBody) {
body.push(line);
continue;
}
if (!line.trim()) {
parsingBody = true;
continue;
}
const [key, ...value] = line.split(':');
if (value.length && key.toLowerCase() !== 'host') headers[key.trim().toLowerCase()] = value.join(':').trim();
}
const formattedHeaders = HEADER_PRIORITY
.filter(h => headers[h])
.map(h => `${capitalizeHeader(h)}: ${headers[h]}`)
.join('\n');
let output = `${protocol} request on ${dpt}\n\n${requestLine}`;
if (formattedHeaders) output += `\n${formattedHeaders}`;
if (requestLineRaw.startsWith('POST')) {
const bodyContent = body.join('\n').trim();
if (bodyContent) output += `\nPOST Data: ${bodyContent}`;
}
return output;
};
const getReportDetails = (entry, dpt) => {
const proto = entry?.attack_connection?.protocol || 'unknown';
const hex = entry?.attack_connection?.payload?.data_hex || '';
const ascii = Buffer.from(hex, 'hex').toString('utf8');
const simplifiedAscii = ascii.replace(/\s+/g, ' ').toLowerCase();
const payloadLen = entry?.attack_connection?.payload?.length || 0;
const flags = createFlagCollection();
let comment;
switch (true) {
case payloadLen === 0:
flags.add(FLAGS.PORT_SCAN);
comment = `Empty payload on ${dpt}/${proto} (likely service probe)`;
break;
case payloadLen > 1000:
flags.add(FLAGS.BAD_WEB_BOT);
comment = `Large payload (${payloadLen} bytes) on ${dpt}/${proto}`;
break;
case (/HTTP\/(0\.9|1\.0|1\.1|2|3)/i).test(simplifiedAscii):
flags.add(FLAGS.HTTP, FLAGS.BAD_WEB_BOT);
comment = parseHttpRequest(ascii, dpt);
break;
case (/\bssh\b/).test(simplifiedAscii):
flags.add(FLAGS.SSH);
comment = `SSH handshake/banner on ${dpt}/${proto} (${payloadLen} bytes of payload)`;
break;
case simplifiedAscii.includes('cookie:'):
flags.add(FLAGS.HTTP, FLAGS.BAD_WEB_BOT);
comment = `HTTP header with cookie on ${dpt}/${proto}`;
break;
case (/(admin|root|wget|curl|bash|eval|php|bin)/).test(simplifiedAscii):
flags.add(FLAGS.COMMAND_INJECTION);
comment = `Suspicious payload on ${dpt}/${proto} (possible command injection)`;
break;
default:
flags.add(FLAGS.PORT_SCAN);
comment = `Unauthorized traffic on ${dpt}/${proto} (${payloadLen} bytes of payload)`;
break;
}
return { proto, baseComment: comment, categories: flags.toString(), timestamp: entry?.['@timestamp'] };
};
const flushBuffer = async reportIp => {
if (!attackBuffer.size) return;
for (const [srcIp, ports] of attackBuffer.entries()) {
const sortedPorts = Array.from(ports.entries())
.sort(([, a], [, b]) => b.count - a.count)
.slice(0, 6);
const proto = sortedPorts[0][1].proto || 'tcp';
const timestamp = sortedPorts[0][1].timestamp;
const baseComment = sortedPorts[0][1].baseComment;
const categories = sortedPorts[0][1].categories;
const portSummary = sortedPorts.map(([port, data]) => `${port} [${data.count}]`).join(', ');
const comment = `Honeypot ${SERVER_ID ? `[${SERVER_ID}]` : 'hit'}: ${baseComment.replace(/ on \d+\/\w+/, '')}; ${portSummary} ${proto.toUpperCase()}`;
await reportIp('HONEYTRAP', { srcIp, dpt: sortedPorts[0][0], proto, timestamp }, categories, comment);
await logIpToFile(srcIp, { honeypot: 'HONEYTRAP', comment });
}
logger.success(`HONEYTRAP -> Flushed ${attackBuffer.size} IPs`);
attackBuffer.clear();
};
module.exports = reportIp => {
tailFile(LOG_FILE, async line => {
if (!line.length) return;
let entry;
try {
entry = JSON.parse(line);
} catch (err) {
return logger.error(`HONEYTRAP -> JSON parse error: ${err.message}\nFaulty line: ${JSON.stringify(line)}`);
}
try {
const srcIp = entry?.attack_connection?.remote_ip;
const dpt = entry?.attack_connection?.local_port;
if (!srcIp || !dpt) return;
let ipData = attackBuffer.get(srcIp);
if (!ipData) {
ipData = new Map();
attackBuffer.set(srcIp, ipData);
}
const { proto, timestamp, categories, baseComment } = getReportDetails(entry, dpt);
let portData = ipData.get(dpt);
if (portData) {
portData.count++;
} else {
portData = { count: 1, proto, timestamp, categories, baseComment };
ipData.set(dpt, portData);
}
logger.info(`HONEYTRAP -> ${srcIp} hit ${dpt} | x${portData.count}`);
} catch (err) {
logger.error(err);
}
}, { label: 'HONEYTRAP' }).catch(err => logger.error(err));
// Clean buffer
const cleanupInterval = setInterval(async () => {
if (Date.now() >= lastFlushTime + 5 * 60 * 1000) {
await flushBuffer(reportIp);
lastFlushTime = Date.now();
}
}, 60 * 1000);
logger.success('🛡️ HONEYTRAP » Watcher initialized');
return {
flush: () => flushBuffer(reportIp),
cleanup: () => {
clearInterval(cleanupInterval);
attackBuffer.clear();
},
};
};
+16
View File
@@ -0,0 +1,16 @@
const { getServerIPs } = require('./services/ipFetcher.js');
let ipPattern = null;
const buildPattern = () => {
const ips = getServerIPs();
if (!ips.length) return null;
const escaped = ips.map(i => i.replace(/[.:\\[\](){}^$*+?|]/g, '\\$&'));
return new RegExp(escaped.join('|'), 'g');
};
module.exports = str => {
if (ipPattern === null) ipPattern = buildPattern() || false;
return ipPattern ? str.replace(ipPattern, '[SOME-IP]') : str;
};
+16
View File
@@ -0,0 +1,16 @@
const ipaddr = require('ipaddr.js');
const LOCAL_RANGES = new Set([
'unspecified', 'multicast', 'linkLocal', 'loopback', 'reserved', 'benchmarking', 'amt',
'broadcast', 'carrierGradeNat', 'private', 'as112',
'uniqueLocal', 'ipv4Mapped', 'rfc6145', '6to4', 'teredo', 'as112v6', 'orchid2', 'droneRemoteIdProtocolEntityTags',
]);
module.exports = ip => {
try {
const range = ipaddr.parse(ip).range();
return { is: LOCAL_RANGES.has(range), range };
} catch {
return { is: false, range: null };
}
};
+25
View File
@@ -0,0 +1,25 @@
const fs = require('node:fs/promises');
const path = require('node:path');
const ipSanitizer = require('./ipSanitizer.js');
const logger = require('../scripts/logger.js');
const resolvePath = require('./pathResolver.js');
const { LOG_IP_HISTORY_ENABLED, LOG_IP_HISTORY_DIR } = require('../config.js').MAIN;
const BASE_DIR = resolvePath(LOG_IP_HISTORY_DIR);
module.exports = async (ip, metadata = {}) => {
if (!LOG_IP_HISTORY_ENABLED || !ip) return;
const { honeypot = 'unknown', comment } = metadata;
const logEntry = `[${new Date().toISOString()}] ${ipSanitizer(comment)}\n`;
const ipDir = path.join(BASE_DIR, ip);
const logFile = path.join(ipDir, `${honeypot.toUpperCase()}.txt`);
try {
await fs.mkdir(ipDir, { recursive: true });
await fs.appendFile(logFile, logEntry);
} catch (err) {
logger.error(`Failed to write IP log for ${ip}: ${err.message}`);
}
};
+151
View File
@@ -0,0 +1,151 @@
const { reset, IS_PM2 } = require('./banners/utils/helper.js');
const { axiosWebhook } = require('./services/axios.js');
const repoInfo = require('./repo.js');
const { SERVER_ID, DISCORD_WEBHOOK_ENABLED, DISCORD_WEBHOOK_URL, DISCORD_WEBHOOK_USERNAME, DISCORD_USER_ID } = require('../config.js').MAIN;
const LEVELS = Object.freeze({
0: { method: 'log', label: '[i]', color: '\x1b[38;5;38m', hex: 0x00AEEF, name: 'INFO' },
1: { method: 'log', label: '[✓]', color: '\x1b[38;5;77m', hex: 0x3CB371, name: 'SUCCESS' },
2: { method: 'warn', label: '[!]', color: '\x1b[38;5;208m', hex: 0xFF8C00, name: 'WARNING' },
3: { method: 'error', label: '[X]', color: '\x1b[38;5;160m', hex: 0xD32F2F, name: 'ERROR' },
});
const WEBHOOK_BURST_LIMIT = 3, WEBHOOK_COOLDOWN = 3000;
const webhookQueue = [];
let webhookBurstCount = 0, webhookLastSent = 0;
class Logger {
static #getLevel(level) {
return LEVELS[level] ?? LEVELS[0];
}
static #normalizeOptions(options, defaultDiscord, defaultPing) {
if (typeof options === 'boolean') return { discord: defaultDiscord, ping: options };
return {
discord: options.discord ?? defaultDiscord,
ping: options.ping ?? defaultPing,
};
}
static print(msg, level = 0) {
const { method, label, color: lvlColor } = this.#getLevel(level);
const fn = console[method] || console.log;
if (IS_PM2) {
fn(`${label} ${msg}`);
} else {
fn(`${lvlColor}${label} ${msg}${reset}`);
}
}
static async #sendToDiscord(msg, hex, ping) {
if (!msg || !DISCORD_WEBHOOK_ENABLED || !DISCORD_WEBHOOK_URL) return;
if (typeof msg !== 'string') msg = msg.toString();
try {
const res = await axiosWebhook.post(DISCORD_WEBHOOK_URL, {
username: DISCORD_WEBHOOK_USERNAME === 'SERVER_ID' ? SERVER_ID : DISCORD_WEBHOOK_USERNAME || null,
content: (ping && DISCORD_USER_ID) ? `**[<@${DISCORD_USER_ID}>]**` : undefined,
embeds: [{
description: msg
.replace(/(\b\w+=)/g, '**$1**')
.replace(/'/g, '`')
.trim(),
color: hex ?? 0x008FD1,
footer: {
text: `${SERVER_ID ? `${SERVER_ID} • ` : ''}${repoInfo.repoSlug} [v${repoInfo.version}]`,
},
timestamp: new Date().toISOString(),
}],
});
if (res.status !== 204) {
this.print(`Failed to deliver Discord Webhook (unexpected status code: ${res.status})`, 2);
}
} catch (err) {
this.print(`Discord Webhook Error: ${err.message}`, 3);
}
}
static async webhook(msg, levelOrHex = 0, ping = false) {
const isLevel = typeof levelOrHex === 'number' && levelOrHex >= 0 && levelOrHex <= 3;
const hex = isLevel ? this.#getLevel(levelOrHex).hex : levelOrHex;
const now = Date.now();
if (now - webhookLastSent > WEBHOOK_COOLDOWN) webhookBurstCount = 0;
if (webhookBurstCount >= WEBHOOK_BURST_LIMIT) {
webhookQueue.push({ msg, hex, ping });
if (webhookQueue.length === 1) {
this.print('Discord webhook rate limit reached. Some messages were scheduled for later delivery.', 2);
setTimeout(() => this.#processWebhookQueue(), WEBHOOK_COOLDOWN);
}
return;
}
try {
await this.#sendToDiscord(msg, hex, ping);
webhookLastSent = now;
webhookBurstCount++;
if (webhookQueue.length > 0) setTimeout(() => this.#processWebhookQueue(), WEBHOOK_COOLDOWN);
} catch (err) {
this.print(`Discord webhook delivery failed: ${err.message}`, 3);
}
}
static async #processWebhookQueue() {
if (webhookQueue.length === 0) return;
if (Date.now() - webhookLastSent > WEBHOOK_COOLDOWN) webhookBurstCount = 0;
const { msg, hex, ping } = webhookQueue.shift();
this.print(`Processing queued webhook (${webhookQueue.length} remaining)`, 0);
try {
await this.#sendToDiscord(msg, hex, ping);
webhookLastSent = Date.now();
webhookBurstCount = 1;
} catch (err) {
this.print(`Queued webhook failed: ${err.message}`, 3);
}
if (webhookQueue.length > 0) setTimeout(() => this.#processWebhookQueue(), WEBHOOK_COOLDOWN);
}
static log(msg, level = 0, options = {}) {
this.print(msg, level);
if (options.discord) {
this.webhook(msg, level, options.ping)
.catch(err => console.error('[Logger] Webhook error:', err.message));
}
}
static info(msg, options = {}) {
const opts = this.#normalizeOptions(options, false, false);
this.log(msg, 0, opts);
}
static success(msg, options = {}) {
const opts = this.#normalizeOptions(options, false, false);
this.log(msg, 1, opts);
}
static warn(msg, options = {}) {
const opts = this.#normalizeOptions(options, true, false);
this.log(msg, 2, opts);
}
static error(msg, options = {}) {
const opts = this.#normalizeOptions(options, true, false);
this.log(msg, 3, opts);
}
static getWebhookQueueSize() {
return webhookQueue.length;
}
}
module.exports = Logger;
+25
View File
@@ -0,0 +1,25 @@
const logger = require('./logger.js');
console.log('=== Testing Logger Rate Limiting ===\n');
// Test 1: Basic logging with timestamps
console.log('Test 1: Basic logging');
logger.info('This is info message', { ping: true });
logger.success('This is success message');
logger.warn('This is warning message');
logger.error('This is error message');
console.log('\n--- Waiting 1 second ---\n');
setTimeout(async () => {
console.log('Test 2: Webhook queue (simulated)');
// Simulate rapid webhook calls
for (let i = 1; i <= 6; i++) {
await logger.webhook(`Test webhook ${i}`, 0, false);
}
console.log(`\nWebhook queue size: ${logger.getWebhookQueueSize()}`);
setTimeout(() => process.exit(0), 40000);
}, 1000);
+12
View File
@@ -0,0 +1,12 @@
const os = require('node:os');
const path = require('node:path');
module.exports = inputPath => {
if (typeof inputPath !== 'string') return inputPath;
// Replace tilde with home directory
const expandedPath = inputPath.replace(/^~(?=$|\/|\\)/, os.homedir());
// Return absolute path
return path.resolve(expandedPath);
};
+33
View File
@@ -0,0 +1,33 @@
const { version, author: authorMeta, repository, license } = require('../package.json');
const githubRepoRegex = /github\.com[:/]+([^/]+)\/([^/#.]+)(?:\.git)?/;
const [, author = '', name = ''] = repository?.url?.match(githubRepoRegex) || [];
const slug = `${author}/${name}`;
const url = `https://github.com/${slug}`;
const nameMappings = {
sniffcat: 'SniffCat',
abuseipdb: 'AbuseIPDB',
};
const getPrettyName = repo => {
const repoLower = repo.toLowerCase();
return Object.entries(nameMappings)
.reduce((best, [key, value]) =>
repoLower.includes(key.toLowerCase()) && key.length > best.key.length
? { key, value }
: best,
{ key: '', value: repo }).value;
};
module.exports = Object.freeze({
version,
repoAuthor: author,
authorMeta,
repoName: name,
prettyName: getPrettyName(name),
repoSlug: slug,
repoUrl: url,
license,
});
+104
View File
@@ -0,0 +1,104 @@
const axios = require('axios');
const { version, repoName, repoUrl } = require('../repo.js');
const { SERVER_ID, EXTENDED_LOGS, SNIFFCAT_API_KEY, ABUSEIPDB_API_KEY, SEFIN_API_SECRET_TOKEN, CLOUDFLARE_API_KEY } = require('../../config.js').MAIN;
// Lazy-load logger
let logger;
const getLogger = () => logger || (logger = require('../logger.js'));
const DEFAULT_HEADERS = {
'User-Agent': `Mozilla/5.0 (compatible; ${repoName}/${version}; +${repoUrl})`,
'Accept': 'application/json',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
};
const HEADERS = {
json: { ...DEFAULT_HEADERS, 'Content-Type': 'application/json' },
multipart: { ...DEFAULT_HEADERS, 'Content-Type': 'multipart/form-data' },
};
const SERVICE_HEADERS = {
sniffcat: { ...HEADERS.json, 'X-Secret-Token': SNIFFCAT_API_KEY },
abuseipdb: { ...HEADERS.json, Key: ABUSEIPDB_API_KEY },
sefinek: { ...DEFAULT_HEADERS, 'X-API-Key': SEFIN_API_SECRET_TOKEN },
cloudflare: { ...HEADERS.json, Authorization: `Bearer ${CLOUDFLARE_API_KEY}` },
};
const BASE_URLS = {
sniffcat: 'https://api.sniffcat.com/api/v1',
abuseipdb: 'https://api.abuseipdb.com/api/v2',
};
const resolveServiceConfig = str => {
const lower = str.toLowerCase();
for (const [key, baseURL] of Object.entries(BASE_URLS)) {
if (lower.includes(key)) return { baseURL, headers: SERVICE_HEADERS[key], serviceKey: key };
}
return null;
};
const serviceConfig = resolveServiceConfig(repoName);
if (!serviceConfig) {
getLogger().log(`No matching baseURL found for repoName '${repoName}', expected one of: ${Object.keys(BASE_URLS).join(', ')}`, 3, true);
process.exit(1);
}
if (SERVER_ID === 'development' && EXTENDED_LOGS && process.argv.length <= 2) {
getLogger().log(`Base URL: ${serviceConfig.baseURL}`);
getLogger().log(JSON.stringify(serviceConfig.headers));
}
// Resolve headers for bulk (multipart + token)
const resolveBulkHeaders = serviceKey => {
if (!serviceKey || !SERVICE_HEADERS[serviceKey]) return HEADERS.multipart;
return { ...SERVICE_HEADERS[serviceKey], 'Content-Type': 'multipart/form-data' };
};
// Axios instances
const axiosGeneric = axios.create({ timeout: 20000, headers: DEFAULT_HEADERS });
const axiosService = axios.create({ baseURL: serviceConfig.baseURL, timeout: 30000, headers: serviceConfig.headers });
const axiosBulk = axios.create({ baseURL: serviceConfig.baseURL, timeout: 55000, headers: resolveBulkHeaders(serviceConfig.serviceKey) });
const axiosSefinek = axios.create({ timeout: 35000, headers: SERVICE_HEADERS.sefinek });
const axiosWebhook = axios.create({ timeout: 15000, headers: DEFAULT_HEADERS });
const axiosCloudflare = axios.create({ baseURL: 'https://api.cloudflare.com/client/v4', timeout: 30000, headers: SERVICE_HEADERS.cloudflare });
// Retry interceptor
const MAX_RETRIES = 3;
const RETRY_DELAY = count => count * 7000;
const RETRYABLE_CODES = new Set(['ECONNABORTED', 'ENOTFOUND', 'ETIMEDOUT']);
const isRetryable = err =>
RETRYABLE_CODES.has(err.code) ||
err.response?.status >= 500;
const hasStream = config => config.data && typeof config.data.pipe === 'function';
const applyRetry = instance => {
instance.interceptors.response.use(null, async err => {
const config = err.config;
if (!config) throw err;
config.__retryCount ??= 0;
if (config.__retryCount >= MAX_RETRIES || !isRetryable(err) || hasStream(config)) throw err;
config.__retryCount++;
const status = err.response?.status
? `Status ${err.response.status}`
: err.code || err.message || 'Unknown error';
getLogger().log(`${status} - retry #${config.__retryCount} for ${config.url}`, 2);
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY(config.__retryCount)));
return instance(config);
});
};
[axiosGeneric, axiosService, axiosBulk, axiosSefinek, axiosWebhook, axiosCloudflare].forEach(applyRetry);
module.exports = Object.freeze({
axiosGeneric, // Generic client without baseURL
axiosService, // Main client for SniffCat, AbuseIPDB
axiosBulk, // Bulk client with multipart/form-data (CSV upload) for SniffCat and AbuseIPDB
axiosSefinek, // Client for Sefinek API
axiosWebhook, // Client for Discord webhooks
axiosCloudflare, // Client for Cloudflare API
HEADERS, // Predefined headers for all services
});
+187
View File
@@ -0,0 +1,187 @@
const FormData = require('form-data');
const { parse } = require('csv-parse/sync');
const { stringify } = require('csv-stringify/sync');
const fs = require('node:fs/promises');
const path = require('node:path');
const { axiosBulk } = require('../services/axios.js');
const { saveReportedIPs, markIPAsReported } = require('../services/cache.js');
const ABUSE_STATE = require('./state.js');
const { queueWrite, atomicWriteFile } = require('./writeQueue.js');
const logger = require('../logger.js');
const BULK_REPORT_BUFFER = new Map();
const BUFFER_FILE = path.join(__dirname, '..', '..', 'tmp', 'bulk-report-buffer.csv');
const OFFLINE_MODE = false;
const ensureDirectoryExists = async filePath => {
try {
await fs.mkdir(path.dirname(filePath), { recursive: true });
} catch (err) {
if (err.code !== 'EEXIST') throw err;
}
};
const saveBufferToFile = () => queueWrite(BUFFER_FILE, async () => {
if (!BULK_REPORT_BUFFER.size) return;
const records = [];
for (const [ip, entry] of BULK_REPORT_BUFFER.entries()) {
records.push([ip, entry.categories, new Date(entry.timestamp).toISOString(), entry.comment]);
}
try {
const output = stringify(records, { header: true, columns: ['IP', 'Categories', 'ReportDate', 'Comment'], quoted: true });
await ensureDirectoryExists(BUFFER_FILE);
await atomicWriteFile(BUFFER_FILE, output);
} catch (err) {
logger.error(`Failed to write buffer file: ${err.message}`, { ping: true });
}
});
const loadBufferFromFile = async () => {
try {
await fs.access(BUFFER_FILE);
const fileContent = await fs.readFile(BUFFER_FILE, 'utf-8');
const records = parse(fileContent, { columns: false, from_line: 2, skip_empty_lines: true, trim: true });
for (const record of records) {
const [ip, categories, timestamp, comment] = record;
if (!ip || !timestamp) continue;
BULK_REPORT_BUFFER.set(ip, { categories, timestamp: new Date(timestamp).getTime(), comment });
}
} catch (err) {
if (err.code !== 'ENOENT') logger.error(`Failed to parse/load buffer file: ${err.message}`);
}
};
const sendBulkReport = async () => {
if (!BULK_REPORT_BUFFER.size) return;
logger.info(`Starting bulk report upload (${BULK_REPORT_BUFFER.size} IPs)...`);
const records = Array.from(BULK_REPORT_BUFFER.entries(), ([ip, entry]) => [
ip,
entry.categories,
new Date(entry.timestamp ?? Date.now()).toISOString(),
entry.comment ? (entry.comment.length > 1024 ? entry.comment.substring(0, 1021) + '...' : entry.comment) : '',
]);
const sendChunk = async (chunk, index = 0, total = 1) => {
const payload = stringify(chunk, {
header: true,
columns: ['IP', 'Categories', 'ReportDate', 'Comment'],
quoted: true,
});
const lines = payload.split('\n').length;
logger.success(`${total > 1 ? `Chunk ${index + 1}/${total}: ` : ''}Generated CSV with ${chunk.length} records, ${lines} lines`);
if (OFFLINE_MODE) {
if (lines > 10000) {
const error = new Error('Line limit exceeded');
error.response = {
status: 422,
data: { errors: [{ detail: `File uploaded has ${lines} lines which exceeds the line limit of 10,000 lines.`, status: 422 }] },
};
throw error;
}
const saved = chunk.length;
logger.success(`${total > 1 ? `Chunk ${index + 1}/${total}: ` : 'Sent bulk report: '}${saved} accepted, 0 rejected [OFFLINE]`, { discord: true });
return;
}
if (lines > 10000) {
const error = new Error('Line limit exceeded');
error.response = {
status: 422,
data: { errors: [{ detail: `File uploaded has ${lines} lines which exceeds the line limit of 10,000 lines.`, status: 422 }] },
};
throw error;
}
const form = new FormData();
form.append('csv', Buffer.from(payload), {
filename: total > 1 ? `report-chunk-${index + 1}.csv` : 'report.csv',
contentType: 'text/csv',
});
const { data } = await axiosBulk.post('/bulk-report', form, { headers: { ...form.getHeaders() } });
const saved = data?.data?.savedReports ?? 0;
const failed = data?.data?.invalidReports?.length ?? 0;
logger.success(`${total > 1 ? `Chunk ${index + 1}/${total}: ` : 'Sent bulk report: '}${saved} accepted, ${failed} rejected`, { discord: true });
if (failed > 0) {
const prefix = total > 1 ? `Chunk ${index + 1}: ` : '';
data.data.invalidReports.forEach(fail => {
logger.warn(`${prefix}Rejected [Row ${fail.rowNumber}] ${fail.input} -> ${fail.error}`);
});
}
};
const isLineLimitError = err =>
err.response?.status === 422 &&
err.response?.data?.errors?.some(e => e.detail.includes('exceeds the line limit'));
let success = false;
try {
await sendChunk(records);
success = true;
} catch (err) {
if (isLineLimitError(err)) {
const chunks = [];
const estimatedLinesPerRecord = 10;
const chunkSize = Math.max(1, Math.floor(9000 / estimatedLinesPerRecord));
for (let i = 0; i < records.length; i += chunkSize) {
chunks.push(records.slice(i, i + chunkSize));
}
logger.info(`File too large. Splitting ${records.length} records into ${chunks.length} chunks (~${chunkSize} records each)`, { discord: true });
let allChunksSuccessful = true;
for (let i = 0; i < chunks.length; i++) {
try {
await sendChunk(chunks[i], i, chunks.length);
} catch (chunkErr) {
logger.error(`Chunk ${i + 1} failed: ${chunkErr.response?.data?.errors?.[0]?.detail || chunkErr.message}`);
allChunksSuccessful = false;
}
}
success = allChunksSuccessful;
} else {
const errorMsg = err.response?.data?.errors
? err.response.data.errors.map(e => e.detail).join(', ')
: err.response?.data ? JSON.stringify(err.response.data) : err.stack;
logger.error(errorMsg);
}
}
if (success) {
for (const ip of BULK_REPORT_BUFFER.keys()) markIPAsReported(ip);
await saveReportedIPs();
BULK_REPORT_BUFFER.clear();
try {
await fs.unlink(BUFFER_FILE);
} catch (err) {
// Ignore file deletion errors - file may not exist or already deleted
if (err.code !== 'ENOENT') logger.warn(`Failed to delete buffer file: ${err.message}`);
}
ABUSE_STATE.sentBulk = true;
} else {
logger.error('Bulk upload failed, keeping buffer file for retry', { ping: true });
}
};
module.exports = Object.freeze({
saveBufferToFile,
loadBufferFromFile,
sendBulkReport,
BULK_REPORT_BUFFER,
});
+80
View File
@@ -0,0 +1,80 @@
const { dirname } = require('node:path');
const fs = require('node:fs/promises');
const resolvePath = require('../pathResolver.js');
const { CACHE_FILE, IP_REPORT_COOLDOWN } = require('../../config.js').MAIN;
const { queueWrite, atomicWriteFile } = require('./writeQueue.js');
const logger = require('../logger.js');
const reportedIPs = new Map();
const RESOLVED_CACHE_FILE = resolvePath(CACHE_FILE);
const ensureCacheDir = async () => {
const dir = dirname(RESOLVED_CACHE_FILE);
try {
await fs.access(dir);
} catch (err) {
if (err.code === 'ENOENT') {
await fs.mkdir(dir, { recursive: true });
logger.success(`Created cache directory: ${dir}`);
} else {
logger.error(`Failed to access cache directory: ${err.stack}`);
}
}
};
const loadReportedIPs = async () => {
if (!RESOLVED_CACHE_FILE) return;
await ensureCacheDir();
try {
const fileContent = await fs.readFile(RESOLVED_CACHE_FILE, 'utf8');
fileContent
.split('\n')
.filter(Boolean)
.forEach(line => {
const [ip, time] = line.trim().split(/\s+/);
if (ip && !isNaN(time)) reportedIPs.set(ip, Number(time));
});
} catch (err) {
if (err.code === 'ENOENT') {
logger.info(`${RESOLVED_CACHE_FILE} does not exist. No data to load.`);
} else {
logger.error(`Failed to load cache file: ${err.stack}`);
}
}
};
const saveReportedIPs = () => {
if (!RESOLVED_CACHE_FILE) return Promise.resolve();
return queueWrite(RESOLVED_CACHE_FILE, async () => {
await ensureCacheDir();
try {
const data = Array.from(reportedIPs)
.map(([ip, time]) => `${ip} ${time}`)
.join('\n');
await atomicWriteFile(RESOLVED_CACHE_FILE, data);
} catch (err) {
logger.error(`Failed to save cache file: ${err.stack}`);
}
});
};
const IP_REPORT_COOLDOWN_SECONDS = IP_REPORT_COOLDOWN / 1000;
const isIPReportedRecently = ip => {
const reportedTime = reportedIPs.get(ip);
if (!reportedTime) return false;
return (Math.floor(Date.now() / 1000) - reportedTime < IP_REPORT_COOLDOWN_SECONDS);
};
const markIPAsReported = ip => reportedIPs.set(ip, Math.floor(Date.now() / 1000));
module.exports = Object.freeze({
reportedIPs,
loadReportedIPs,
saveReportedIPs,
isIPReportedRecently,
markIPAsReported,
});
+144
View File
@@ -0,0 +1,144 @@
const fs = require('node:fs/promises');
const path = require('node:path');
const { existsSync } = require('node:fs');
const { parse } = require('csv-parse/sync');
const { stringify } = require('csv-stringify/sync');
const logger = require('../../logger.js');
const CSV_FILE = path.join(__dirname, '..', '..', '..', 'tmp', 'reported_ips.csv');
const CSV_COLUMNS = ['Timestamp', 'CF RayID', 'IP', 'Country', 'Hostname', 'Endpoint', 'User-Agent', 'Action taken', 'Status', 'Sefinek API'];
const MAX_CSV_SIZE = 1024 * 1024;
const CSV_STRINGIFY_OPTS = {
header: true,
columns: CSV_COLUMNS,
record_delimiter: '\n',
quoted: true,
quoted_empty: true,
quoted_string: true,
};
const CSV_PARSE_OPTS = {
columns: true,
skip_empty_lines: true,
trim: false,
skip_records_with_empty_values: false,
};
const ensureCSVExists = async () => {
try {
if (existsSync(CSV_FILE)) return;
await fs.mkdir(path.dirname(CSV_FILE), { recursive: true });
await fs.writeFile(CSV_FILE, stringify([], CSV_STRINGIFY_OPTS));
logger.success(`Created missing CSV file: ${CSV_FILE}`);
} catch (err) {
logger.error(`Failed to ensure CSV exists: ${err.stack}`);
throw err;
}
};
const checkCSVSize = async () => {
try {
if (!existsSync(CSV_FILE)) return;
const stats = await fs.stat(CSV_FILE);
if (stats.size > MAX_CSV_SIZE) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupPath = `${CSV_FILE}.${timestamp}.bkp`;
await fs.copyFile(CSV_FILE, backupPath);
await fs.writeFile(CSV_FILE, stringify([], CSV_STRINGIFY_OPTS));
logger.success(`CSV size limit exceeded (${(MAX_CSV_SIZE / (1024 * 1024)).toFixed(2)} MB). Backup saved as "${path.basename(backupPath)}", original file cleared.`);
}
} catch (err) {
logger.error(`Failed to check or clear CSV size: ${err.stack}`);
}
};
const logToCSV = async (event, status = 'N/A', sefinekAPI = false) => {
try {
await ensureCSVExists();
await checkCSVSize();
const {
rayName, clientIP, clientCountryName,
clientRequestHTTPHost, clientRequestPath,
userAgent, action,
} = event;
const row = {
'Timestamp': new Date().toISOString(),
'CF RayID': rayName,
'IP': clientIP,
'Country': clientCountryName,
'Hostname': clientRequestHTTPHost,
'Endpoint': clientRequestPath,
'User-Agent': userAgent,
'Action taken': action.toUpperCase(),
'Status': status,
'Sefinek API': String(sefinekAPI),
};
const line = stringify([row], { ...CSV_STRINGIFY_OPTS, header: false });
await fs.appendFile(CSV_FILE, line);
} catch (err) {
logger.error(`Failed to log event to CSV: ${err.stack}`);
}
};
const readReportedIPs = async () => {
if (!existsSync(CSV_FILE)) return [];
try {
const content = await fs.readFile(CSV_FILE, 'utf-8');
const records = parse(content, CSV_PARSE_OPTS);
return records.map(row => ({
timestamp: Date.parse(row['Timestamp']),
rayId: row['CF RayID'],
ip: row['IP'],
country: row['Country'],
hostname: row['Hostname'],
endpoint: row['Endpoint'],
userAgent: row['User-Agent'],
action: row['Action taken'],
status: row['Status'],
sefinekAPI: row['Sefinek API'] === 'true',
}));
} catch (err) {
logger.error(`Failed to parse or map CSV content: ${err.stack}`);
return [];
}
};
const batchUpdateSefinekAPIInCSV = async (rayIds = []) => {
if (!existsSync(CSV_FILE) || !rayIds.length) return;
try {
const content = await fs.readFile(CSV_FILE, 'utf-8');
const records = parse(content, CSV_PARSE_OPTS);
const raySet = new Set(rayIds);
let updated = false;
for (const row of records) {
if (raySet.has(row['CF RayID']) && row['Sefinek API'] !== 'true') {
row['Sefinek API'] = 'true';
updated = true;
}
}
if (updated) {
const output = stringify(records, CSV_STRINGIFY_OPTS);
await fs.writeFile(CSV_FILE, output);
}
} catch (err) {
logger.error(`Batch CSV update failed: ${err.stack}`);
}
};
module.exports = Object.freeze({
logToCSV,
readReportedIPs,
batchUpdateSefinekAPIInCSV,
});
@@ -0,0 +1,55 @@
const { MAIN } = require('../../../config.js');
module.exports = zoneTag => {
if (!zoneTag) throw new Error('zoneTag is null or undefined');
const variables = {
zoneTag,
filter: {
datetime_geq: new Date(Date.now() - MAIN.CLOUDFLARE_TIME_RANGE || 12 * 60 * 60 * 1000).toISOString(),
AND: [
{ action_neq: 'allow' },
{ action_neq: 'skip' },
{ action_neq: 'challenge_solved' },
{ action_neq: 'challenge_failed' },
{ action_neq: 'challenge_bypassed' },
{ action_neq: 'jschallenge_solved' },
{ action_neq: 'jschallenge_failed' },
{ action_neq: 'jschallenge_bypassed' },
{ action_neq: 'managed_challenge_skipped' },
{ action_neq: 'managed_challenge_non_interactive_solved' },
{ action_neq: 'managed_challenge_interactive_solved' },
{ action_neq: 'managed_challenge_bypassed' },
],
},
};
return { query: `
query ListFirewallEvents($zoneTag: string, $filter: FirewallEventsAdaptiveFilter_InputObject) {
viewer {
zones(filter: { zoneTag: $zoneTag }) {
firewallEventsAdaptive(
filter: $filter,
limit: ${MAIN.CLOUDFLARE_EVENTS_LIMIT || 3000},
orderBy: [datetime_DESC]
) {
action
clientASNDescription
clientAsn
clientCountryName
clientIP
clientRequestHTTPHost
clientRequestHTTPMethodName
clientRequestHTTPProtocol
clientRequestPath
clientRequestQuery
datetime
rayName
ruleId
source
userAgent
}
}
}
}`, variables };
};
@@ -0,0 +1,64 @@
const fs = require('node:fs');
const path = require('node:path');
const PAYLOAD = require('./generateFirewallQuery.js');
const { axiosCloudflare } = require('../axios.js');
const logger = require('../../logger.js');
const { MAIN } = require('../../../config.js');
const ensureFile = filePath => {
if (fs.existsSync(filePath)) return;
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, '');
};
const fetchCloudflareEvents = async () => {
const rawZoneIds = MAIN.CLOUDFLARE_ZONE_IDS || MAIN.CLOUDFLARE_ZONE_ID;
const zoneIds = Array.isArray(rawZoneIds) ? rawZoneIds : [rawZoneIds];
const allEvents = [];
for (const zoneId of zoneIds) {
try {
const { data, status } = await axiosCloudflare.post('/graphql', PAYLOAD(zoneId));
const events = data?.data?.viewer?.zones?.[0]?.firewallEventsAdaptive;
if (!events) throw new Error(`Failed to retrieve data from Cloudflare (status ${status}): ${JSON.stringify(data?.errors)}`);
const l7ddosEvents = events.filter(e => e.source === 'l7ddos');
logger.success(`Zone ${zoneId}: ${events.length} events fetched (${l7ddosEvents.length} L7 DDoS)`);
allEvents.push(...l7ddosEvents);
} catch (err) {
logger.error(err.response?.data ? `${err.response.status} HTTP ERROR for zone ${zoneId}: ${JSON.stringify(err.response.data, null, 2)}` : `Unknown error for zone ${zoneId}: ${err.message}`);
}
}
return allEvents;
};
const saveToCSV = (events, filePath = 'report.csv') => {
const map = new Map();
for (const { clientIP, datetime } of events) {
if (!map.has(clientIP)) map.set(clientIP, `${clientIP},4,${new Date(datetime).toISOString()},Detected L7 DDoS via Cloudflare`);
}
ensureFile(filePath);
if (!fs.statSync(filePath).size) fs.appendFileSync(filePath, 'IP,Categories,ReportDate,Comment\n');
fs.appendFileSync(filePath, [...map.values()].join('\n') + '\n');
};
const saveToTXT = (events, filePath = 'report.txt') => {
const uniqueIPs = new Set(events.map(e => e.clientIP));
ensureFile(filePath);
fs.appendFileSync(filePath, [...uniqueIPs].join('\n') + '\n');
};
(async () => {
const events = await fetchCloudflareEvents();
if (events.length) {
saveToCSV(events);
saveToTXT(events);
logger.success(`Saved ${events.length} L7 DDoS events to CSV and TXT`);
} else {
logger.info('No L7 DDoS events found');
}
})();
@@ -0,0 +1,22 @@
const { axiosGeneric } = require('../axios.js');
const logger = require('../../logger.js');
module.exports = async () => {
try {
const res = await axiosGeneric.get('https://api.sefinek.net/api/v2/filter-rules');
const data = res.data;
if (!data.success || typeof data !== 'object') {
throw new Error(`Sefinek API error: Invalid response (success=${data.success})`);
}
const { userAgents, domains, endpoints, imgExtensions, lastUpdate } = data;
if (!Array.isArray(userAgents) || !Array.isArray(domains) || !Array.isArray(endpoints) || !Array.isArray(imgExtensions)) {
throw new Error('Sefinek API error: Response is missing expected arrays');
}
logger.success(`Filter rules loaded from api.sefinek.net, last update: ${lastUpdate}`);
return { userAgents, domains, endpoints, imgExtensions, lastUpdate };
} catch (err) {
throw new Error(`Failed to fetch filter rules: ${err.message}`, { cause: err });
}
};
@@ -0,0 +1,56 @@
const FormData = require('form-data');
const logger = require('../../logger.js');
const { readReportedIPs, batchUpdateSefinekAPIInCSV } = require('./csv.js');
const { axiosSefinek } = require('../axios.js');
const { getServerIPs } = require('../ipFetcher.js');
module.exports = async () => {
const serverIPs = getServerIPs();
const reportedIPs = (await readReportedIPs() || []).filter(x =>
['REPORTED', 'READY_FOR_BULK_REPORT', 'RL_BULK_REPORT'].includes(x.status) &&
!serverIPs.includes(x.ip) &&
!x.sefinekAPI
);
if (!reportedIPs.length) return logger.info('Sefinek API: No data to report');
const seenIPs = new Set();
const uniqueLogs = reportedIPs.filter(ip => {
if (seenIPs.has(ip.ip)) return false;
seenIPs.add(ip.ip);
return true;
});
if (!uniqueLogs.length) return logger.info(`Sefinek API: No unique IPs to send (reportedIPs = ${reportedIPs.length}; uniqueLogs = ${uniqueLogs.length})`);
try {
const payload = uniqueLogs.map(ip => ({
rayId: ip.rayId,
ip: ip.ip,
endpoint: ip.endpoint,
userAgent: ip.userAgent,
action: ip.action,
country: ip.country,
timestamp: ip.timestamp,
}));
const base64 = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64');
const form = new FormData();
form.append('file', base64, {
filename: 'reports.dat',
contentType: 'application/octet-stream',
});
const res = await axiosSefinek.post('https://api.sefinek.net/api/v2/cloudflare-waf-abuseipdb', form, { headers: { ...form.getHeaders() } });
// const res = await axiosSefinek.post('http://127.0.0.1:4010/api/v2/cloudflare-waf-abuseipdb', form, { headers: { ...form.getHeaders() } });
if (res.data.success) {
logger.success(`Sefinek API (status: ${res.status}): Successfully sent ${uniqueLogs.length} logs`);
await batchUpdateSefinekAPIInCSV(uniqueLogs.map(x => x.rayId));
} else {
logger.warn(`Sefinek API (status: ${res.status}): ${res.data.message || 'Something went wrong'}`);
}
} catch (err) {
if (err.response?.data?.message?.includes('No valid or unique')) return;
const msg = err.response?.data?.message ?? err.message;
logger.error(`Sefinek API (status: ${err.response?.status ?? 'unknown'}): Failed to send logs! Message: ${typeof msg === 'object' ? JSON.stringify(msg) : msg}`, { ping: true });
}
};
+84
View File
@@ -0,0 +1,84 @@
const { networkInterfaces } = require('node:os');
const https = require('node:https');
const { CronJob } = require('cron');
const { axiosSefinek } = require('./axios.js');
const isSpecialPurposeIP = require('../isSpecialPurposeIP.js');
const logger = require('../logger.js');
const { IP_ASSIGNMENT, IP_REFRESH_SCHEDULE, IPv6_SUPPORT } = require('../../config.js').MAIN;
const ipAddresses = new Set();
let ipv6ErrorCount = 0, ipv6ErrorLogged = false;
let isRefreshing = false;
const fetchLocalIPs = () => {
for (const ifaceList of Object.values(networkInterfaces())) {
for (const iface of ifaceList) {
const addr = iface?.address;
if (addr && !iface.internal && !isSpecialPurposeIP(addr).is) ipAddresses.add(addr);
}
}
};
const fetchIPAddress = async (family, attempt = 1) => {
if (family === 0) return fetchLocalIPs();
if (family === 6 && (!IPv6_SUPPORT || ipv6ErrorLogged)) return;
try {
const { data } = await axiosSefinek.get('https://api.sefinek.net/api/v2/ip', {
httpsAgent: new https.Agent({ family }),
});
if (data?.success && data?.message) {
ipAddresses.add(data.message);
if (family === 6 && ipv6ErrorCount > 0) {
logger.success(`IPv6 is now working. It succeeded after ${ipv6ErrorCount} failed attempts.`);
ipv6ErrorCount = 0;
ipv6ErrorLogged = false;
}
} else {
logger.warn(`Unexpected API response: success=${data?.success}, message=${data?.message}`);
}
} catch (err) {
logger.error(`Failed to fetch IPv${family} address (attempt ${attempt}): ${err.message}`);
if (family === 6) {
ipv6ErrorCount++;
if (ipv6ErrorCount >= 6 && !ipv6ErrorLogged) {
ipv6ErrorLogged = true;
logger.warn('IPv6 address could not be retrieved after multiple attempts. Disabling further checks.');
} else if (attempt < 6) {
await new Promise(resolve => setTimeout(resolve, 4000));
await fetchIPAddress(6, attempt + 1);
}
}
}
};
const refreshServerIPs = async () => {
if (isRefreshing) {
logger.info('IP refresh already in progress, skipping...');
return;
}
isRefreshing = true;
logger.info('Trying to fetch your IPv4 and IPv6 address from api.sefinek.net...');
try {
await Promise.all([fetchIPAddress(0), fetchIPAddress(4), fetchIPAddress(6)]);
} catch (err) {
logger.error(`Failed to refresh IP addresses: ${err.message}`);
} finally {
isRefreshing = false;
}
};
(async () => {
if (IP_ASSIGNMENT === 'dynamic') new CronJob(IP_REFRESH_SCHEDULE || '0 */6 * * *', refreshServerIPs, null, true);
})();
module.exports = Object.freeze({
refreshServerIPs,
getServerIPs: () => [...ipAddresses],
});
+41
View File
@@ -0,0 +1,41 @@
const { exec } = require('node:child_process');
const ecosystem = require('../../ecosystem.config.js');
const logger = require('../logger.js');
const EXEC_TIMEOUT = 60000;
const executeCmd = cmd => new Promise((resolve, reject) => {
exec(cmd, { timeout: EXEC_TIMEOUT }, (err, stdout, stderr) => {
if (err) return reject(err);
if (stderr) logger.warn(`Warning while executing: ${cmd}\n${stderr}`);
resolve(stdout.trim());
});
});
const CMD_1 = 'npm install --omit=dev';
const APP_NAME = (() => {
if (!ecosystem.apps?.[0]?.name) throw new Error('Missing app name in ecosystem config');
return ecosystem.apps[0].name;
})();
const CMD_2 = `pm2 restart ${APP_NAME}`;
module.exports = async () => {
try {
// 1 - npm dependencies
logger.info(`Running npm '${CMD_1}'...`);
const result1 = await executeCmd(CMD_1);
logger.info(result1, { discord: true });
// 2 - restart
if (process.env.pm_id !== undefined) {
logger.info(`Running '${CMD_2}'...`);
await executeCmd(CMD_2);
} else {
logger.info('Process is not managed by PM2! Exiting to apply updates. To start again, run: node . (one-time), pm2 start (24/7 with auto-restart).', { discord: true });
process.exit(0);
}
} catch (err) {
logger.error(err.stack);
}
};
+1
View File
@@ -0,0 +1 @@
module.exports = { isLimited: false, isBuffering: false, sentBulk: false };
+99
View File
@@ -0,0 +1,99 @@
const { CronJob } = require('cron');
const fs = require('node:fs/promises');
const logger = require('../logger.js');
const resolvePath = require('../pathResolver.js');
const { SERVER_ID, EXTENDED_LOGS, CACHE_FILE } = require('../../config.js').MAIN;
const RESOLVED_CACHE_FILE = resolvePath(CACHE_FILE);
const pad = n => n.toString().padStart(2, '0');
const formatHourRange = h => `${pad(h)}:00-${pad(h)}:59`;
const pluralizeReport = n => (n === 1 ? 'report' : 'reports');
const summaryEmbed = async () => {
const now = new Date();
const midnightLocal = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const yesterday = new Date(midnightLocal.getTime() - 86400000);
const yesterdayString = `${yesterday.getFullYear()}-${pad(yesterday.getMonth() + 1)}-${pad(yesterday.getDate())}`;
const baseTs = Math.floor(Date.UTC(yesterday.getFullYear(), yesterday.getMonth(), yesterday.getDate()) / 1000);
if (EXTENDED_LOGS) {
logger.info(`Daily summary » Using base timestamp: ${baseTs} (${new Date(baseTs * 1000).toISOString()}) for ${yesterdayString}`);
}
let data;
if (SERVER_ID === 'development') {
logger.info('Daily summary » Using test data instead of reading cache file [DEV]');
const generateRandomIp = () => Array(4).fill(0).map(() => Math.floor(Math.random() * 256)).join('.');
const generateTimestamps = () => baseTs + Math.floor(Math.random() * 86400);
const entries = Array.from({ length: Math.floor(Math.random() * 1001) + 4000 }, () => `${generateRandomIp()} ${generateTimestamps()}`);
data = entries.join('\n');
} else {
try {
await fs.access(RESOLVED_CACHE_FILE);
data = (await fs.readFile(RESOLVED_CACHE_FILE, 'utf8')).trim();
if (!data) return logger.warn(`Daily summary » Cache file exists but is empty: ${RESOLVED_CACHE_FILE}`);
} catch (err) {
return logger.warn(`Daily summary » Failed to access/read cache file: ${err.message}`);
}
}
try {
const hourlySummary = {};
const seen = new Set();
for (const line of data.split('\n')) {
const [ip, tsStr] = line.split(' ');
const ts = parseInt(tsStr, 10);
if (!ip || isNaN(ts)) continue;
const key = `${ip}_${ts}`;
if (seen.has(key)) continue;
seen.add(key);
const date = new Date(ts * 1000);
const dateStr = date.toISOString().split('T')[0];
if (dateStr !== yesterdayString) continue;
const hour = date.getUTCHours();
hourlySummary[hour] = (hourlySummary[hour] || 0) + 1;
}
if (!Object.keys(hourlySummary).length) {
return logger.warn(`Daily summary » No reports found for ${yesterdayString}`);
}
const sortedChrono = Object.entries(hourlySummary)
.map(([h, c]) => [parseInt(h), c])
.sort((a, b) => a[0] - b[0]);
const top3Sorted = [...Object.entries(hourlySummary)]
.map(([h, c]) => [parseInt(h), c])
.sort((a, b) => b[1] - a[1])
.slice(0, 3);
const top3 = top3Sorted.map(([h]) => h);
const total = sortedChrono.reduce((sum, [, c]) => sum + c, 0);
const summaryStr = sortedChrono
.map(([h, c]) => `${formatHourRange(h)} → ${c} ${pluralizeReport(c)}${top3.includes(h) ? ' 🔥' : ''}`)
.join('\n');
logger.info(`Midnight. Summary of IP address reports (${total}) from ${yesterdayString}:\n${summaryStr}`);
const peakStr = top3Sorted
.map(([h, c]) => `${formatHourRange(h)} → ${c} ${pluralizeReport(c)}`)
.join('\n');
logger.info(`Top 3 peaks:\n${peakStr}`);
await logger.webhook(
`Midnight. Summary of IP address reports from yesterday.\nGood night to you, sleep well! 😴\n### ${total} ${pluralizeReport(total)} from ${yesterdayString}\n\`\`\`${summaryStr}\`\`\`\n### Top 3 peaks\n\`\`\`${peakStr}\`\`\``,
0x00FF39
);
} catch (err) {
logger.error(`Daily summary » Unexpected error:\n${err.stack}`);
}
};
module.exports = async () => {
// await summaryEmbed();
new CronJob('0 0 * * *', summaryEmbed, null, true);
};
+60
View File
@@ -0,0 +1,60 @@
const fs = require('node:fs');
const path = require('node:path');
const TailFile = require('@logdna/tail-file');
const split2 = require('split2');
const logger = require('../logger.js');
/**
* Starts tailing a log file, calling onLine for each new line.
* If the file does not exist, waits for it to be created (directory must exist).
*
* @param {string} filePath - Path to the log file.
* @param {Function} onLine - Callback invoked for each new line (receives the line string).
* @param {Object} [options]
* @param {string} [options.label] - Label for log messages (e.g., 'DIONAEA', 'SURICATA').
* @returns {Promise<TailFile|null>} Resolves with the TailFile instance once tailing starts, or null on error.
*/
module.exports = (filePath, onLine, options = {}) => {
const { label } = options;
const prefix = label ? `${label} -> ` : '';
const start = async () => {
const tail = new TailFile(filePath);
const onError = err => logger.error(`${prefix}${err.message || err}`);
tail.on('tail_error', onError);
tail.on('error', onError);
await tail.start();
tail.pipe(split2()).on('data', onLine).on('error', onError);
return tail;
};
if (fs.existsSync(filePath)) return start();
const resolved = path.resolve(filePath);
const dir = path.dirname(resolved);
const filename = path.basename(resolved);
if (!fs.existsSync(dir)) {
logger.error(`${prefix}Log file directory does not exist: ${dir}`);
return Promise.resolve(null);
}
logger.warn(`${prefix}Log file does not exist yet: ${filePath}. Waiting for it to be created...`);
return new Promise(resolve => {
const watcher = fs.watch(dir, (event, file) => {
if (file === filename && fs.existsSync(filePath)) {
watcher.close();
logger.info(`${prefix}Log file detected: ${filePath}. Starting tail...`);
resolve(start());
}
});
watcher.on('error', err => {
logger.error(`${prefix}Failed to watch directory ${dir}: ${err.message}`);
resolve(null);
});
});
};
+91
View File
@@ -0,0 +1,91 @@
const simpleGit = require('simple-git');
const semver = require('semver');
const { CronJob } = require('cron');
const fs = require('node:fs');
const path = require('node:path');
const restartApp = require('./restartApp.js');
const logger = require('../logger.js');
const { SERVER_ID, AUTO_UPDATE_SCHEDULE, EXTENDED_LOGS } = require('../../config.js').MAIN;
const git = simpleGit();
const pkgPath = path.resolve(__dirname, '../../package.json');
const LOCK_FILE = path.resolve(__dirname, '../../.git/index.lock');
const STALE_LOCK_THRESHOLD = 60_000;
const getLocalVersion = () => {
delete require.cache[require.resolve(pkgPath)];
return require(pkgPath).version;
};
const cleanStaleLock = () => {
try {
const stat = fs.statSync(LOCK_FILE);
if (Date.now() - stat.mtimeMs > STALE_LOCK_THRESHOLD) {
fs.unlinkSync(LOCK_FILE);
logger.warn('Removed stale .git/index.lock file', { discord: true });
}
} catch (err) {
if (err.code !== 'ENOENT') logger.error(`Failed to check/remove index.lock: ${err.message}`);
}
};
const pull = async () => {
try {
if (SERVER_ID !== 'development') {
if (EXTENDED_LOGS) logger.info('Resetting local repository to HEAD (--hard)...');
await git.reset(['--hard']);
}
logger.info('Pulling the repository and the required submodule...');
// Get submodule status before update
const submoduleBefore = await git.raw(['submodule', 'status']);
// Pull main repo and update submodules
const pullResult = await git.pull();
await git.submoduleUpdate(['--init', '--recursive', '--remote', '--merge']);
// Get submodule status after update
const submoduleAfter = await git.raw(['submodule', 'status']);
const submoduleChanged = submoduleBefore !== submoduleAfter;
const { changes, insertions, deletions } = pullResult.summary;
const mainRepoChanged = changes > 0 || insertions > 0 || deletions > 0;
const hasChanges = mainRepoChanged || submoduleChanged;
if (hasChanges) {
const parts = [];
if (mainRepoChanged) parts.push(`Main repo - Changes: ${changes}; Insertions: ${insertions}; Deletions: ${deletions};`);
if (submoduleChanged) parts.push('Submodule updated.');
logger.success(`Updates pulled successfully! ${parts.join(' ')}`, { discord: true });
} else {
logger.info('No new updates detected');
}
return hasChanges;
} catch (err) {
logger.error(err.stack);
return null;
}
};
const pullAndRestart = async () => {
try {
cleanStaleLock();
const oldVersion = getLocalVersion();
const hasChanges = await pull();
if (!hasChanges) return;
const newVersion = getLocalVersion();
if (semver.neq(newVersion, oldVersion)) {
logger.success(`Version changed: ${oldVersion} → ${newVersion}`, { discord: true });
}
await restartApp();
} catch (err) {
logger.error(err.stack);
}
};
new CronJob(AUTO_UPDATE_SCHEDULE, pullAndRestart, null, true);
module.exports = pullAndRestart;
+22
View File
@@ -0,0 +1,22 @@
const semver = require('semver');
const { axiosGeneric } = require('./axios.js');
const { version, repoSlug } = require('../repo.js');
const logger = require('../logger.js');
const PACKAGE_JSON_URL = `https://raw.githubusercontent.com/${repoSlug}/main/package.json`;
(async () => {
logger.info('Checking for new versions...');
try {
const { data: { version: latest } } = await axiosGeneric.get(PACKAGE_JSON_URL);
if (semver.gt(latest, version)) {
logger.info(`A new version is available! v${version} → v${latest}. Please update.`, { discord: true });
} else if (semver.gt(version, latest)) {
logger.info(`Local version v${version} is ahead of remote v${latest}`, { discord: true });
}
} catch (err) {
logger.error(`Version check failed: ${err.stack}`);
}
})();
+154
View File
@@ -0,0 +1,154 @@
const fs = require('node:fs');
const path = require('node:path');
const logger = require('../logger.js');
const isSpecialPurposeIP = require('../isSpecialPurposeIP.js');
const filePath = path.resolve(__dirname, '..', '..', 'whitelist.txt');
let whitelistedIPs = new Set();
let whitelistedPorts = new Set(); // @chillcog Port Whitelist
let reloadTimer = null;
const parse = content => {
// @chillcog Port Whitelist
const ipSet = new Set();
const portSet = new Set();
const invalidIPs = [], skippedIPs = [];
const invalidPorts = [], skippedPorts = [];
let section = 'ip';
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
const raw = lines[i];
const header = raw.trim().toLowerCase();
if (header.includes('[port whitelist]') || (header.includes('port whitelist') && header.includes('['))) {
section = 'port';
continue;
}
if (header.includes('[ip whitelist]') || (header.includes('ip whitelist') && header.includes('['))) {
section = 'ip';
continue;
}
const line = raw.split('#')[0].trim();
if (!line) continue;
if (section === 'port') {
const portStr = String(line).split(':').pop().split('/')[0].trim();
const port = Number(portStr);
if (!Number.isInteger(port) || port < 1 || port > 65535) {
invalidPorts.push(`${line} (line ${i + 1})`);
continue;
}
if (portSet.has(port)) {
skippedPorts.push(`${line} (line ${i + 1})`);
continue;
}
portSet.add(port);
continue;
}
const { is, range } = isSpecialPurposeIP(line);
if (range === null) {
invalidIPs.push(`${line} (line ${i + 1})`);
continue;
}
if (is) {
skippedIPs.push(`${line} (${range})`);
continue;
}
ipSet.add(line);
}
if (invalidIPs.length > 0) {
logger.warn(`Invalid IP address${invalidIPs.length > 1 ? 'es' : ''} in whitelist, skipping: ${invalidIPs.join(', ')}`);
}
if (skippedIPs.length > 0) {
logger.warn(`Special-purpose IP${skippedIPs.length > 1 ? 's' : ''} detected in whitelist.txt, was this intentional? ${skippedIPs.join(', ')}`);
}
if (invalidPorts.length > 0) {
logger.warn(`Invalid port${invalidPorts.length > 1 ? 's' : ''} in whitelist, skipping: ${invalidPorts.join(', ')}`);
}
if (skippedPorts.length > 0) {
logger.warn(`Skipped duplicate port${skippedPorts.length > 1 ? 's' : ''} in whitelist.txt: ${skippedPorts.join(', ')}`);
}
return { ipSet, portSet };
};
const load = () => {
try {
const content = fs.readFileSync(filePath, 'utf-8');
const { ipSet, portSet } = parse(content);
whitelistedIPs = ipSet;
whitelistedPorts = portSet;
if (whitelistedIPs.size > 0) logger.info(`Loaded ${whitelistedIPs.size} whitelisted IP${whitelistedIPs.size > 1 ? 's' : ''} from ${filePath}`);
if (whitelistedPorts.size > 0) logger.info(`Loaded ${whitelistedPorts.size} whitelisted port${whitelistedPorts.size > 1 ? 's' : ''} from ${filePath}`);
} catch (err) {
logger.error(`Failed to load whitelist file: ${err.message}`);
}
};
const initWhitelist = () => {
if (!fs.existsSync(filePath)) {
fs.writeFileSync(
filePath,
[
'# UFW AbuseIPDB Reporter - whitelist.txt',
'# Changes to this file are detected and applied automatically (no restart required)',
'',
'# ====== [IP WHITELIST] ======',
'# One IP address per line (IPv4 or IPv6)',
'# Lines starting with # are comments',
'#',
'# Examples:',
'# 79.186.0.0',
'# 2a01:11bf:4504:b10c:8a32:ffe7:510a:6d4e',
'',
'# ====== [PORT WHITELIST] ======',
'# One destination port per line. Applies to ALL IPs.',
'# Optional: you may write entries like "22/tcp"; the numeric port will be extracted.',
'# Added if you have certain services that trigger false positives and you want to exclude',
'# them from triggering reports regardless of source IP. Use with caution, as this will ignore',
'# all traffic to the specified port(s) and may cause you to miss legitimate reports if that port is attacked.',
'# Examples:',
'# 22',
'# 25565',
'# 1194/tcp',
].join('\n') + '\n'
);
logger.info(`Created default whitelist file: ${filePath}`);
}
// @chillcog end;
load();
const dir = path.dirname(filePath);
const filename = path.basename(filePath);
try {
const watcher = fs.watch(dir, (event, file) => {
if (file !== filename) return;
clearTimeout(reloadTimer);
reloadTimer = setTimeout(load, 200);
});
watcher.on('error', err => logger.error(`Whitelist watcher error: ${err.message}`));
} catch (err) {
logger.error(`Failed to watch whitelist file: ${err.message}`);
}
};
module.exports = {
initWhitelist,
isWhitelisted: ip => whitelistedIPs.has(ip),
isPortWhitelisted: port => whitelistedPorts.has(port),
};
+20
View File
@@ -0,0 +1,20 @@
const fs = require('node:fs/promises');
const path = require('node:path');
const queues = new Map();
const queueWrite = (filePath, fn) => {
const key = path.resolve(filePath);
const prev = queues.get(key) || Promise.resolve();
const next = prev.then(fn, fn);
queues.set(key, next);
return next;
};
const atomicWriteFile = async (filePath, data) => {
const tmp = `${filePath}.tmp`;
await fs.writeFile(tmp, data);
await fs.rename(tmp, filePath);
};
module.exports = { queueWrite, atomicWriteFile };
+68
View File
@@ -0,0 +1,68 @@
// =====================================================================================================================
// Global Suricata Signature Blacklist
// Signatures listed here will NOT be reported. These are typically false positives from legitimate services.
//
// WARNING: Do not modify this file directly!
// To add custom signatures, use the IGNORED_SIGNATURES array in your config.js file.
// To disable this built-in list entirely, set USE_BUILT_IN_IGNORED_SIGNATURES to false.
//
// Last modified: 13.02.2026
// =====================================================================================================================
const { USE_BUILT_IN_IGNORED_SIGNATURES, IGNORED_SIGNATURES } = require('../../config.js').MAIN;
const GLOBAL_IGNORED = {
// ==========================
// Discord
// ==========================
2035465: 'ET INFO Observed Discord Domain (discord.gg)',
2060503: 'ET INFO Observed Discord Domain in TLS SNI',
// ==========================
// Steam
// ==========================
2028651: 'ET GAMES Steam Client HTTP Session',
// ==========================
// Android Connectivity Check
// ==========================
2036220: 'ET INFO Android Device Connectivity Check',
// ==========================
// DDNS Services (Dynamic DNS)
// ==========================
2043238: 'ET INFO Observed DDNS Domain',
2013743: 'ET POLICY Observed DDNS Provider Domain',
// ==========================
// Cloudflare Tunnel (argotunnel.com)
// ==========================
2047122: 'ET INFO Cloudflare Argo Tunnel Domain',
// ==========================
// Xiaomi
// ==========================
2018918: 'ET INFO possible Xiaomi phone data leakage DNS',
};
const localIgnored = new Set(IGNORED_SIGNATURES || []);
/**
* Check if signature should be ignored
* @param {number} sigId - Signature ID to check
* @returns {boolean}
*/
const isIgnored = sigId => (USE_BUILT_IN_IGNORED_SIGNATURES && sigId in GLOBAL_IGNORED) || localIgnored.has(sigId);
/**
* Get description for ignored signature
* @param {number} sigId - Signature ID
* @returns {string|null}
*/
const getDescription = sigId => {
if (USE_BUILT_IN_IGNORED_SIGNATURES && sigId in GLOBAL_IGNORED) return GLOBAL_IGNORED[sigId];
if (localIgnored.has(sigId)) return 'Local config';
return null;
};
module.exports = { GLOBAL_IGNORED, isIgnored, getDescription };
+11
View File
@@ -0,0 +1,11 @@
module.exports = [
{ key: 'RUN_ON_START', type: Boolean },
{ key: 'CLOUDFLARE_ZONE_IDS', type: Array, nonEmpty: true },
{ key: 'CLOUDFLARE_API_KEY', type: String, nonEmpty: true },
{ key: 'REPORT_SCHEDULE', type: String, nonEmpty: true },
{ key: 'MAX_URL_LENGTH', type: Number, min: 100 },
{ key: 'SUCCESS_COOLDOWN', type: Number, min: 0 },
{ key: 'CLOUDFLARE_TIME_RANGE', type: Number, min: 1 },
{ key: 'CLOUDFLARE_EVENTS_LIMIT', type: Number, min: 1, max: 10000 },
{ key: 'ALLOWED_SOURCES', type: Array, nonEmpty: true },
];
+17
View File
@@ -0,0 +1,17 @@
exports.rules = [
{ key: 'IP_ASSIGNMENT', type: String, oneOf: ['static', 'dynamic'] },
{ key: 'IPv6_SUPPORT', type: Boolean },
{ key: 'EXTENDED_LOGS', type: Boolean },
{ key: 'CACHE_FILE', type: String, nonEmpty: true },
{ key: 'AUTO_UPDATE_ENABLED', type: Boolean },
{ key: 'AUTO_UPDATE_SCHEDULE', type: String, required: false },
{ key: 'DISCORD_WEBHOOK_ENABLED', type: Boolean },
{ key: 'DISCORD_WEBHOOK_URL', type: String, required: false },
];
exports.services = {
sniffcat: { apiKey: 'SNIFFCAT_API_KEY', minCooldown: 20 * 60 * 1000 },
abuseipdb: { apiKey: 'ABUSEIPDB_API_KEY', minCooldown: 15 * 60 * 1000 },
};
+33
View File
@@ -0,0 +1,33 @@
const { repoName } = require('../repo.js');
const validateConfig = require('./validateConfig.js');
const { rules: common, services } = require('./common.js');
const PROJECT_RULES = {
suricata: require('./suricata.js'),
ufw: require('./ufw.js'),
't-pot': require('./tpot.js'),
cloudflare: require('./cloudflare.js'),
};
const resolveByRepoName = (map, name) => {
const lower = name.toLowerCase();
for (const [key, value] of Object.entries(map)) {
if (lower.includes(key)) return value;
}
return null;
};
module.exports = config => {
const rules = [...common];
const serviceConf = resolveByRepoName(services, repoName);
if (serviceConf) {
rules.push({ key: serviceConf.apiKey, type: String, nonEmpty: true });
rules.push({ key: 'IP_REPORT_COOLDOWN', type: Number, min: serviceConf.minCooldown });
}
const projectRules = resolveByRepoName(PROJECT_RULES, repoName);
if (projectRules) rules.push(...projectRules);
validateConfig(config, rules);
};
+7
View File
@@ -0,0 +1,7 @@
module.exports = [
{ key: 'SURICATA_EVE_FILE', type: String, nonEmpty: true },
{ key: 'MIN_ALERT_SEVERITY', type: Number, min: 1, max: 3 },
{ key: 'ALLOWED_VERDICT_ACTIONS', type: Array, nonEmpty: true, oneOf: ['alert', 'pass', 'drop'], default: ['alert', 'drop'] },
{ key: 'USE_BUILT_IN_IGNORED_SIGNATURES', type: Boolean },
{ key: 'IGNORED_SIGNATURES', type: Array },
];
+6
View File
@@ -0,0 +1,6 @@
module.exports = [
{ key: 'COWRIE_LOG_FILE', type: String, nonEmpty: true },
{ key: 'DIONAEA_LOG_FILE', type: String, nonEmpty: true },
{ key: 'HONEYTRAP_LOG_FILE', type: String, nonEmpty: true },
{ key: 'LOG_IP_HISTORY_ENABLED', type: Boolean },
];
+3
View File
@@ -0,0 +1,3 @@
module.exports = [
{ key: 'UFW_LOG_FILE', type: String, nonEmpty: true },
];
+170
View File
@@ -0,0 +1,170 @@
const path = require('node:path');
const logger = require('../logger.js');
const configPath = path.resolve(__dirname, '..', '..', 'config.js');
const TYPE_NAMES = new Map([
[String, 'string'],
[Number, 'number'],
[Boolean, 'boolean'],
[Array, 'array'],
]);
const RULE_SCHEMA = {
key: { type: 'string', required: true },
type: { required: true },
required: { type: 'boolean' },
nonEmpty: { type: 'boolean', validFor: ['string', 'array'] },
oneOf: { isArray: true },
min: { type: 'number', validFor: ['number', 'array'] },
max: { type: 'number', validFor: ['number'] },
default: {},
};
const VALID_RULE_KEYS = new Set(Object.keys(RULE_SCHEMA));
/**
* Validates a rule definition itself before using it to check config values.
* @param {Object} rule
* @param {number} index
* @returns {string[]} Array of error messages (empty if valid).
*/
const validateRule = (rule, index) => {
const errors = [];
const prefix = `Rule #${index}`;
if (!rule || typeof rule !== 'object' || Array.isArray(rule)) return [`${prefix}: must be a plain object`];
const unknownKeys = Object.keys(rule).filter(k => !VALID_RULE_KEYS.has(k));
if (unknownKeys.length > 0) errors.push(`${prefix} (${rule.key || '?'}): unknown properties: ${unknownKeys.join(', ')}`);
if (typeof rule.key !== 'string' || rule.key.length === 0) {
return [...errors, `${prefix}: 'key' must be a non-empty string`];
}
const label = `${prefix} (${rule.key})`;
const typeName = TYPE_NAMES.get(rule.type);
if (!typeName) return [...errors, `${label}: 'type' must be String, Number, Boolean, or Array`];
// Validate rule property types and applicability
for (const [prop, schema] of Object.entries(RULE_SCHEMA)) {
if (rule[prop] === undefined || prop === 'key' || prop === 'type' || prop === 'default') continue;
if (schema.type && typeof rule[prop] !== schema.type) {
errors.push(`${label}: '${prop}' must be ${schema.type}, got ${typeof rule[prop]}`);
} else if (schema.isArray && !Array.isArray(rule[prop])) {
errors.push(`${label}: '${prop}' must be an array`);
}
if (schema.validFor && !schema.validFor.includes(typeName)) {
errors.push(`${label}: '${prop}' is only valid for ${schema.validFor.join(' and ')} types`);
}
}
if (rule.min !== undefined && rule.max !== undefined && typeName === 'number' && rule.min > rule.max) {
errors.push(`${label}: 'min' (${rule.min}) cannot be greater than 'max' (${rule.max})`);
}
// Validate default value
if ('default' in rule) {
const def = rule.default;
const isCorrectType = typeName === 'array' ? Array.isArray(def) : typeof def === typeName;
if (!isCorrectType) {
errors.push(`${label}: 'default' must be ${typeName === 'array' ? 'an array' : `of type '${typeName}'`}, got '${typeof def}'`);
} else {
if (rule.nonEmpty && (typeName === 'array' ? def.length === 0 : typeof def === 'string' && def.trim().length === 0)) {
errors.push(`${label}: 'default' must not be empty`);
}
if (rule.oneOf && Array.isArray(rule.oneOf)) {
if (typeName === 'array') {
const invalid = def.filter(v => !rule.oneOf.includes(v));
if (invalid.length > 0) errors.push(`${label}: 'default' contains invalid value(s): ${invalid.join(', ')}`);
} else if (!rule.oneOf.includes(def)) {
errors.push(`${label}: 'default' must be one of ${rule.oneOf.join(', ')}, got '${def}'`);
}
}
}
}
return errors;
};
/**
* @param {Object} config - The configuration object to validate (e.g., config.MAIN).
* @param {Array<Object>} rules - Array of validation rules.
* @param {string} rules[].key - The config key to validate.
* @param {Function} rules[].type - Expected type constructor: String, Number, Boolean, Array.
* @param {boolean} [rules[].required=true] - Whether the key must be present and non-null.
* @param {boolean} [rules[].nonEmpty] - For strings/arrays: must not be empty.
* @param {Array} [rules[].oneOf] - Allowed values (for primitive types) or allowed items (for arrays).
* @param {number} [rules[].min] - Minimum value (for numbers) or minimum length (for arrays).
* @param {number} [rules[].max] - Maximum value (for numbers).
* @param {*} [rules[].default] - Default value to assign if the key is undefined or null.
*/
const validateConfig = (config, rules) => {
const ruleErrors = rules.flatMap((rule, i) => validateRule(rule, i));
if (ruleErrors.length > 0) throw new Error(`Invalid validation rules:\n${ruleErrors.map(e => ` - ${e}`).join('\n')}`);
const errors = [];
for (const rule of rules) {
const { key, required = true, nonEmpty, oneOf, min, max } = rule;
const value = config[key];
const typeName = TYPE_NAMES.get(rule.type);
if (value === undefined || value === null) {
if ('default' in rule) {
config[key] = rule.default;
continue;
}
if (required) errors.push(`'${key}' is required but is ${value === null ? 'null' : 'undefined'}`);
continue;
}
if (typeName === 'array') {
if (!Array.isArray(value)) {
errors.push(`'${key}' must be an array, got ${typeof value}`);
continue;
}
if (nonEmpty && value.length === 0) {
errors.push(`'${key}' must be a non-empty array`);
}
if (typeof min === 'number' && value.length < min) {
errors.push(`'${key}' must have at least ${min} item(s), got ${value.length}`);
}
if (oneOf) {
const invalid = value.filter(v => !oneOf.includes(v));
if (invalid.length > 0) errors.push(`'${key}' contains invalid value(s): ${invalid.join(', ')}. Allowed: ${oneOf.join(', ')}`);
}
} else {
if (typeof value !== typeName) {
errors.push(`'${key}' must be of type '${typeName}', got '${typeof value}'`);
continue;
}
if (typeName === 'string' && nonEmpty && value.trim().length === 0) {
errors.push(`'${key}' must be a non-empty string`);
}
if (typeName === 'number') {
if (typeof min === 'number' && value < min) errors.push(`'${key}' must be >= ${min}, got ${value}`);
if (typeof max === 'number' && value > max) errors.push(`'${key}' must be <= ${max}, got ${value}`);
}
if (oneOf && !oneOf.includes(value)) {
errors.push(`'${key}' must be one of ${oneOf.join(', ')}, got '${value}'`);
}
}
}
if (errors.length > 0) {
logger.error(`Configuration validation failed (${errors.length}). Please check your config file: ${configPath}\n${errors.map(e => ` - ${e}`).join('\n')}`, { discord: false });
process.exit(1);
}
};
module.exports = validateConfig;