Unlink scripts submodule and commit contents as normal directory
This commit is contained in:
1 parent
4f7dea111f
commit
6488b3ea85
48 files changed
+2876
-1
No files matched your search
-1
Submodule scripts deleted from 190e59627e.
@@ -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.
|
||||
@@ -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 |
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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('');
|
||||
};
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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')); // []
|
||||
@@ -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';
|
||||
}
|
||||
};
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -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');
|
||||
};
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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 };
|
||||
}
|
||||
};
|
||||
@@ -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}`);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
@@ -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],
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = { isLimited: false, isBuffering: false, sentBulk: false };
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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}`);
|
||||
}
|
||||
})();
|
||||
@@ -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),
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 },
|
||||
];
|
||||
@@ -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 },
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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 },
|
||||
];
|
||||
@@ -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 },
|
||||
];
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = [
|
||||
{ key: 'UFW_LOG_FILE', type: String, nonEmpty: true },
|
||||
];
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user