@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.
- package/LICENSE +19 -0
- package/Readme.md +118 -0
- package/bin/daemon.js +155 -0
- 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
|
+
}
|