@roflsec/fail2scan 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +19 -0
  2. package/Readme.md +118 -0
  3. package/bin/daemon.js +155 -0
  4. package/package.json +23 -0
package/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ The LulzHat License v1.0
2
+
3
+ Copyright (c) 2025 RoflSec
4
+
5
+ Permission is hereby granted, free of charge, to any human, robot, or sentient AI
6
+ that obtains a copy of this software and associated documentation files (the "Software"),
7
+ to do whatever the hell they want but ONLY for educational purposes, legal pentesting,
8
+ or making your cat look cooler on the internet.
9
+
10
+ YOU CANNOT use this software to be an actual jerk: hacking random people, destroying stuff,
11
+ or getting arrested is strictly forbidden. If you ignore this, congratulations,
12
+ you are legally and morally dumb.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
15
+ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
16
+ PURPOSE, OR NOT GETTING SUED. USE AT YOUR OWN RISK, AND MAY THE LULZ BE WITH YOU.
17
+
18
+ By using this software, you pledge to only cause chaos (in a lab, a VM, or a controlled environment, or your mum's computer).
19
+ Breaking real laws voids your permission and might make you cry in court, the software creator can't be held responsible.
package/Readme.md ADDED
@@ -0,0 +1,118 @@
1
+
2
+
3
+ ---
4
+
5
+ Fail2Scan
6
+
7
+ Fail2Scan is a Node.js daemon that watches your Fail2Ban logs for banned IP addresses and automatically scans them using system tools (nmap, dig, whois). All results are saved in a structured folder for easy review.
8
+
9
+
10
+ ---
11
+
12
+ Features
13
+
14
+ Watches Fail2Ban logs in real time.
15
+
16
+ Detects new banned IPs automatically.
17
+
18
+ Runs nmap for full port scanning.
19
+
20
+ Runs dig for reverse DNS lookup.
21
+
22
+ Runs whois for IP ownership and ASN info.
23
+
24
+ Saves output in /var/log/fail2scan/<YYYY-MM-DD>/<IP>_<timestamp>/.
25
+
26
+ Pure Node.js, no external dependencies, works with Node 18+.
27
+
28
+ Compatible with PM2 or any process manager.
29
+
30
+
31
+
32
+ ---
33
+
34
+ Installation
35
+
36
+ # Install globally
37
+ sudo npm install -g @roflsec/fail2scan
38
+
39
+
40
+ ---
41
+
42
+ Usage
43
+
44
+ # Start daemon (default settings)
45
+ fail2scan-daemon
46
+
47
+ # Custom log file, output directory, concurrency, nmap arguments, quiet mode
48
+ fail2scan-daemon --log /var/log/fail2ban.log --out /var/log/fail2scan --concurrency 2 --nmap-args "-sS -Pn -p- -T4 -sV" --quiet
49
+
50
+ CLI Options
51
+
52
+ Option Default Description
53
+
54
+ --log /var/log/fail2ban.log Path to your Fail2Ban log file.
55
+ --out /var/log/fail2scan Output directory for scan results.
56
+ --concurrency 1 Number of scans to run in parallel.
57
+ --nmap-args -sS -Pn -p- -T4 -sV Arguments to pass to nmap.
58
+ --quiet false Suppress console output.
59
+ --help / -h Show usage info.
60
+
61
+
62
+
63
+ ---
64
+
65
+ Output Structure
66
+
67
+ Results are saved in this format:
68
+
69
+ /var/log/fail2scan/
70
+ └─ 2025-10-12/
71
+ └─ 192.168.1.100_2025-10-12T14-30-00Z/
72
+ ├─ nmap.txt # raw nmap output
73
+ ├─ dig.txt # raw dig output
74
+ ├─ whois.txt # raw whois output
75
+ └─ summary.json # JSON summary of scan results
76
+
77
+ summary.json includes:
78
+
79
+ ip – scanned IP
80
+
81
+ timestamp – ISO timestamp of scan
82
+
83
+ commands – details of each scan (nmap, dig, whois)
84
+
85
+ open_ports – array of open ports detected by nmap
86
+
87
+
88
+
89
+ ---
90
+
91
+ Requirements
92
+
93
+ Node.js 18+
94
+
95
+ System tools installed: nmap, dig, whois
96
+
97
+ Permissions to read Fail2Ban logs and write to the output directory
98
+
99
+
100
+ # Debian/Ubuntu example
101
+ sudo apt install nmap dnsutils whois
102
+
103
+
104
+ ---
105
+
106
+ Running with PM2
107
+
108
+ # Start daemon with PM2
109
+ pm2 start $(which fail2scan-daemon) -- --log /var/log/fail2ban.log --out /var/log/fail2scan
110
+ pm2 save
111
+ pm2 status
112
+
113
+
114
+ ---
115
+
116
+ License
117
+
118
+ MIT © RoflSec
package/bin/daemon.js ADDED
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Fail2Scan v0.0.1
6
+ * Watches Fail2Ban logs for "Ban <IP>" entries and scans the banned IPs using
7
+ * system tools (nmap, dig, whois). Results are saved to /var/log/fail2scan/<date>/<ip>_<ts>/
8
+ *
9
+ * Node 18+, CommonJS, no external dependencies.
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { execFile } = require('child_process');
15
+ const { promisify } = require('util');
16
+
17
+ const execFileP = promisify(execFile);
18
+ const argv = process.argv.slice(2);
19
+
20
+ function getArg(key, def) {
21
+ for (let i = 0; i < argv.length; i++) {
22
+ const a = argv[i];
23
+ if (a === key && argv[i + 1]) return argv[++i];
24
+ if (a.startsWith(key + '=')) return a.split('=')[1];
25
+ }
26
+ return def;
27
+ }
28
+
29
+ if (argv.includes('--help') || argv.includes('-h')) {
30
+ console.log('Usage: fail2scan-daemon [--log /path/to/fail2ban.log] [--out /path/to/output] [--concurrency N] [--nmap-args "args"] [--quiet]');
31
+ process.exit(0);
32
+ }
33
+
34
+ const LOG_PATH = getArg('--log', '/var/log/fail2ban.log');
35
+ const OUT_ROOT = getArg('--out', '/var/log/fail2scan');
36
+ const CONCURRENCY = Math.max(1, parseInt(getArg('--concurrency', '1'), 10) || 1);
37
+ const NMAP_ARGS_STR = getArg('--nmap-args', '-sS -Pn -p- -T4 -sV');
38
+ const QUIET = argv.includes('--quiet');
39
+
40
+ function log(...args) { if (!QUIET) console.log(new Date().toISOString(), ...args); }
41
+
42
+ function safeMkdirSync(p) { fs.mkdirSync(p, { recursive: true, mode: 0o750 }); }
43
+ async function which(bin) { try { await execFileP('which', [bin]); return true; } catch { return false; } }
44
+ async function runCmd(cmd, args, opts = {}) {
45
+ try {
46
+ const { stdout, stderr } = await execFileP(cmd, args, { maxBuffer: 1024 * 1024 * 32, ...opts });
47
+ return { ok: true, stdout, stderr };
48
+ } catch (e) {
49
+ return { ok: false, stdout: e.stdout || '', stderr: e.stderr || e.message };
50
+ }
51
+ }
52
+
53
+ function sanitizeFilename(s) { return s.replace(/[:\/\\<>?"|* ]+/g, '_'); }
54
+
55
+ async function checkPrereqs() {
56
+ const tools = ['nmap', 'dig', 'whois'];
57
+ for (const t of tools) if (!(await which(t))) {
58
+ console.error(`Missing required binary: ${t}. Please install it (e.g. apt install ${t}).`);
59
+ process.exit(2);
60
+ }
61
+ }
62
+ checkPrereqs();
63
+
64
+ class FileTail {
65
+ constructor(filePath, onLine) {
66
+ this.filePath = filePath;
67
+ this.onLine = onLine;
68
+ this.position = 0;
69
+ this.inode = null;
70
+ this.buffer = '';
71
+ this.watch = null;
72
+ this.start();
73
+ }
74
+
75
+ async start() {
76
+ try {
77
+ const st = fs.statSync(this.filePath);
78
+ this.inode = st.ino;
79
+ this.position = st.size;
80
+ } catch {}
81
+ this.watch = fs.watch(this.filePath, { persistent: true }, async () => {
82
+ try {
83
+ const st = fs.statSync(this.filePath);
84
+ if (st.size < this.position) this.position = 0;
85
+ if (st.size === this.position) return;
86
+ const stream = fs.createReadStream(this.filePath, { start: this.position, end: st.size - 1, encoding: 'utf8' });
87
+ for await (const chunk of stream) {
88
+ this.buffer += chunk;
89
+ let idx;
90
+ while ((idx = this.buffer.indexOf('\n')) >= 0) {
91
+ const line = this.buffer.slice(0, idx);
92
+ this.buffer = this.buffer.slice(idx + 1);
93
+ if (line.trim()) this.onLine(line);
94
+ }
95
+ }
96
+ this.position = st.size;
97
+ } catch {}
98
+ });
99
+ }
100
+
101
+ close() { try { if (this.watch) this.watch.close(); } catch {} }
102
+ }
103
+
104
+ class ScanQueue {
105
+ constructor(concurrency = 1) { this.c = concurrency; this.r = 0; this.q = []; this.set = new Set(); }
106
+ push(ip) { if (this.set.has(ip)) return; this.set.add(ip); this.q.push(ip); this.next(); }
107
+ next() {
108
+ if (this.r >= this.c) return;
109
+ const ip = this.q.shift(); if (!ip) return;
110
+ this.r++; this.run(ip).finally(() => { this.r--; this.set.delete(ip); this.next(); });
111
+ }
112
+ async run(ip) {
113
+ try { log('Scanning', ip); await performScan(ip); log('Done', ip); }
114
+ catch (e) { console.error('Error scanning', ip, e); }
115
+ }
116
+ }
117
+
118
+ const IPV4 = '(?:\\d{1,3}\\.){3}\\d{1,3}';
119
+ const IPV6 = '(?:[0-9a-fA-F:]+)';
120
+ const IP_RE = new RegExp(`(${IPV4}|${IPV6})`);
121
+ const BAN_RE = new RegExp('\\bBan\\b.*(' + IPV4 + '|' + IPV6 + ')', 'i');
122
+
123
+ async function performScan(ip) {
124
+ const now = new Date();
125
+ const dateDir = now.toISOString().slice(0, 10);
126
+ const safeIp = sanitizeFilename(ip);
127
+ const outDir = path.join(OUT_ROOT, dateDir, `${safeIp}_${now.toISOString().replace(/[:.]/g, '-')}`);
128
+ safeMkdirSync(outDir);
129
+
130
+ const summary = { ip, ts: now.toISOString(), cmds: {} };
131
+
132
+ const nmapRes = await runCmd('nmap', NMAP_ARGS_STR.trim().split(/\s+/).concat([ip]));
133
+ fs.writeFileSync(path.join(outDir, 'nmap.txt'), nmapRes.stdout + (nmapRes.stderr ? '\n\n' + nmapRes.stderr : ''));
134
+ summary.cmds.nmap = { ok: nmapRes.ok };
135
+
136
+ const digRes = await runCmd('dig', ['-x', ip, '+short']);
137
+ fs.writeFileSync(path.join(outDir, 'dig.txt'), digRes.stdout + (digRes.stderr ? '\n\n' + digRes.stderr : ''));
138
+ summary.cmds.dig = { ok: digRes.ok };
139
+
140
+ const whoisRes = await runCmd('whois', [ip]);
141
+ fs.writeFileSync(path.join(outDir, 'whois.txt'), whoisRes.stdout + (whoisRes.stderr ? '\n\n' + whoisRes.stderr : ''));
142
+ summary.cmds.whois = { ok: whoisRes.ok };
143
+
144
+ fs.writeFileSync(path.join(outDir, 'summary.json'), JSON.stringify(summary, null, 2));
145
+ }
146
+
147
+ const q = new ScanQueue(CONCURRENCY);
148
+ log('Fail2Scan daemon started on', LOG_PATH);
149
+ const tail = new FileTail(LOG_PATH, (line) => {
150
+ const match = BAN_RE.exec(line);
151
+ if (match && match[1]) q.push(match[1]);
152
+ });
153
+
154
+ process.on('SIGINT', () => { log('Stopping...'); tail.close(); process.exit(0); });
155
+ process.on('SIGTERM', () => { log('Stopping...'); tail.close(); process.exit(0); });
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@roflsec/fail2scan",
3
+ "version": "0.0.1",
4
+ "description": "Fail2Scan daemon - watches fail2ban logs and scans banned IPs using nmap, dig and whois.",
5
+ "bin": {
6
+ "fail2scan-daemon": "./bin/daemon.js"
7
+ },
8
+ "preferGlobal": true,
9
+ "files": [
10
+ "bin/",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "scripts": {
15
+ "start": "node ./bin/daemon.js",
16
+ "test": "node ./bin/daemon.js --help"
17
+ },
18
+ "author": "RoflSecurity",
19
+ "license": "MIT",
20
+ "engines": {
21
+ "node": ">=18"
22
+ }
23
+ }