@roflsec/fail2scan 0.0.1 → 0.0.2
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/bin/daemon.js +132 -70
- package/package.json +1 -1
package/bin/daemon.js
CHANGED
@@ -2,11 +2,12 @@
|
|
2
2
|
'use strict';
|
3
3
|
|
4
4
|
/**
|
5
|
-
* Fail2Scan v0.0.1
|
6
|
-
* Watches Fail2Ban
|
7
|
-
* system tools (nmap, dig, whois). Results are saved to /var/log/fail2scan/<date>/<ip>_<ts>/
|
8
|
-
*
|
5
|
+
* Fail2Scan v0.0.1 - daemon.js
|
6
|
+
* Watches Fail2Ban log for ban events, queues IPs and scans them with nmap/dig/whois.
|
9
7
|
* Node 18+, CommonJS, no external dependencies.
|
8
|
+
*
|
9
|
+
* Usage:
|
10
|
+
* fail2scan-daemon --log /var/log/fail2ban.log --out /var/log/fail2scan --concurrency 1 --nmap-args "-sS -Pn -p- -T4 -sV" --quiet
|
10
11
|
*/
|
11
12
|
|
12
13
|
const fs = require('fs');
|
@@ -17,6 +18,7 @@ const { promisify } = require('util');
|
|
17
18
|
const execFileP = promisify(execFile);
|
18
19
|
const argv = process.argv.slice(2);
|
19
20
|
|
21
|
+
// --------- CLI helpers ----------
|
20
22
|
function getArg(key, def) {
|
21
23
|
for (let i = 0; i < argv.length; i++) {
|
22
24
|
const a = argv[i];
|
@@ -27,7 +29,7 @@ function getArg(key, def) {
|
|
27
29
|
}
|
28
30
|
|
29
31
|
if (argv.includes('--help') || argv.includes('-h')) {
|
30
|
-
console.log('
|
32
|
+
console.log('Fail2Scan daemon\n--log PATH (default /var/log/fail2ban.log)\n--out PATH (default /var/log/fail2scan)\n--concurrency N (default 1)\n--nmap-args "args" (default "-sS -Pn -p- -T4 -sV")\n--quiet');
|
31
33
|
process.exit(0);
|
32
34
|
}
|
33
35
|
|
@@ -39,28 +41,36 @@ const QUIET = argv.includes('--quiet');
|
|
39
41
|
|
40
42
|
function log(...args) { if (!QUIET) console.log(new Date().toISOString(), ...args); }
|
41
43
|
|
44
|
+
// --------- Utilities ----------
|
42
45
|
function safeMkdirSync(p) { fs.mkdirSync(p, { recursive: true, mode: 0o750 }); }
|
43
|
-
|
46
|
+
|
47
|
+
async function which(bin) {
|
48
|
+
try { await execFileP('which', [bin]); return true; } catch { return false; }
|
49
|
+
}
|
50
|
+
|
44
51
|
async function runCmd(cmd, args, opts = {}) {
|
45
52
|
try {
|
46
53
|
const { stdout, stderr } = await execFileP(cmd, args, { maxBuffer: 1024 * 1024 * 32, ...opts });
|
47
|
-
return { ok: true, stdout, stderr };
|
54
|
+
return { ok: true, stdout: stdout || '', stderr: stderr || '' };
|
48
55
|
} catch (e) {
|
49
|
-
return { ok: false, stdout: e.stdout || '', stderr: e.stderr || e.message };
|
56
|
+
return { ok: false, stdout: (e.stdout || '') + '', stderr: (e.stderr || e.message) + '' };
|
50
57
|
}
|
51
58
|
}
|
52
59
|
|
53
|
-
function sanitizeFilename(s) { return s.replace(/[:\/\\<>?"|* ]+/g, '_'); }
|
60
|
+
function sanitizeFilename(s) { return String(s).replace(/[:\/\\<>?"|* ]+/g, '_'); }
|
54
61
|
|
55
|
-
|
62
|
+
// --------- Prerequisites check ----------
|
63
|
+
(async function checkPrereqs() {
|
56
64
|
const tools = ['nmap', 'dig', 'whois'];
|
57
|
-
for (const t of tools)
|
58
|
-
|
59
|
-
|
65
|
+
for (const t of tools) {
|
66
|
+
if (!(await which(t))) {
|
67
|
+
console.error(`Missing required binary: ${t}. Install it (eg: apt install ${t}).`);
|
68
|
+
process.exit(2);
|
69
|
+
}
|
60
70
|
}
|
61
|
-
}
|
62
|
-
checkPrereqs();
|
71
|
+
})().catch(e => { console.error('Prereq check failed', e); process.exit(2); });
|
63
72
|
|
73
|
+
// --------- File tail (handles rotation) ----------
|
64
74
|
class FileTail {
|
65
75
|
constructor(filePath, onLine) {
|
66
76
|
this.filePath = filePath;
|
@@ -68,88 +78,140 @@ class FileTail {
|
|
68
78
|
this.position = 0;
|
69
79
|
this.inode = null;
|
70
80
|
this.buffer = '';
|
71
|
-
this.
|
72
|
-
this.start();
|
81
|
+
this.watcher = null;
|
82
|
+
this.start().catch(err => { console.error('Tail start error', err); process.exit(1); });
|
73
83
|
}
|
74
84
|
|
75
85
|
async start() {
|
86
|
+
try { const st = fs.statSync(this.filePath); this.inode = st.ino; this.position = st.size; } catch (e) { this.inode = null; this.position = 0; }
|
87
|
+
this._watchFile();
|
88
|
+
await this._readNew();
|
89
|
+
}
|
90
|
+
|
91
|
+
_watchFile() {
|
92
|
+
try {
|
93
|
+
this.watcher = fs.watch(this.filePath, { persistent: true }, async () => {
|
94
|
+
try {
|
95
|
+
let st; try { st = fs.statSync(this.filePath); } catch { st = null; }
|
96
|
+
if (!st) { this.inode = null; this.position = 0; return; }
|
97
|
+
if (this.inode !== null && st.ino !== this.inode) { this.inode = st.ino; this.position = 0; }
|
98
|
+
else if (this.inode === null) { this.inode = st.ino; this.position = 0; }
|
99
|
+
await this._readNew();
|
100
|
+
} catch (err) {}
|
101
|
+
});
|
102
|
+
} catch (e) { console.error('fs.watch failed:', e.message); process.exit(1); }
|
103
|
+
}
|
104
|
+
|
105
|
+
async _readNew() {
|
76
106
|
try {
|
77
107
|
const st = fs.statSync(this.filePath);
|
78
|
-
this.
|
79
|
-
this.position
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
-
}
|
108
|
+
if (st.size < this.position) this.position = 0;
|
109
|
+
if (st.size === this.position) return;
|
110
|
+
const stream = fs.createReadStream(this.filePath, { start: this.position, end: st.size - 1, encoding: 'utf8' });
|
111
|
+
for await (const chunk of stream) {
|
112
|
+
this.buffer += chunk;
|
113
|
+
let idx;
|
114
|
+
while ((idx = this.buffer.indexOf('\n')) >= 0) {
|
115
|
+
const line = this.buffer.slice(0, idx);
|
116
|
+
this.buffer = this.buffer.slice(idx + 1);
|
117
|
+
if (line.trim()) this.onLine(line);
|
95
118
|
}
|
96
|
-
|
97
|
-
|
98
|
-
})
|
119
|
+
}
|
120
|
+
this.position = st.size;
|
121
|
+
} catch (e) {}
|
99
122
|
}
|
100
123
|
|
101
|
-
close() { try { if (this.
|
124
|
+
close() { try { if (this.watcher) this.watcher.close(); } catch (e) {} }
|
102
125
|
}
|
103
126
|
|
127
|
+
// --------- Scan queue ----------
|
104
128
|
class ScanQueue {
|
105
|
-
constructor(concurrency = 1) { this.
|
106
|
-
push(ip) {
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
129
|
+
constructor(concurrency = 1) { this.concurrency = concurrency; this.running = 0; this.queue = []; this.set = new Set(); }
|
130
|
+
push(ip) {
|
131
|
+
if (this.set.has(ip)) { log('IP already queued or running:', ip); return; }
|
132
|
+
this.set.add(ip); this.queue.push(ip); this._next();
|
133
|
+
}
|
134
|
+
_next() {
|
135
|
+
if (this.running >= this.concurrency) return;
|
136
|
+
const ip = this.queue.shift();
|
137
|
+
if (!ip) return;
|
138
|
+
this.running++;
|
139
|
+
this._run(ip).finally(() => { this.running--; this.set.delete(ip); setImmediate(() => this._next()); });
|
111
140
|
}
|
112
|
-
async
|
113
|
-
try { log('Scanning', ip); await performScan(ip); log('Done', ip); }
|
114
|
-
catch (e) { console.error('Error scanning', ip, e); }
|
141
|
+
async _run(ip) {
|
142
|
+
try { log('Scanning', ip); await performScan(ip); log('Done', ip); } catch (e) { console.error('Error scanning', ip, e); }
|
115
143
|
}
|
116
144
|
}
|
117
145
|
|
118
|
-
|
119
|
-
const
|
120
|
-
|
121
|
-
const
|
146
|
+
function extractIpFromLine(line) {
|
147
|
+
const ipv4 = line.match(/\b(?:\d{1,3}\.){3}\d{1,3}\b/);
|
148
|
+
if (ipv4 && ipv4[0]) return ipv4[0];
|
149
|
+
const ipv6 = line.match(/\b([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}\b/);
|
150
|
+
if (ipv6 && ipv6[0]) return ipv6[0];
|
151
|
+
return null;
|
152
|
+
}
|
122
153
|
|
154
|
+
// --------- Scan logic ----------
|
123
155
|
async function performScan(ip) {
|
124
156
|
const now = new Date();
|
157
|
+
const iso = now.toISOString().replace(/\..+$/, '').replace(/:/g, '-');
|
125
158
|
const dateDir = now.toISOString().slice(0, 10);
|
126
159
|
const safeIp = sanitizeFilename(ip);
|
127
|
-
const outDir = path.join(OUT_ROOT, dateDir, `${safeIp}_${
|
128
|
-
safeMkdirSync(outDir);
|
129
|
-
|
130
|
-
const summary = { ip,
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
160
|
+
const outDir = path.join(OUT_ROOT, dateDir, `${safeIp}_${iso}`);
|
161
|
+
try { safeMkdirSync(outDir); } catch (e) { throw new Error('Cannot create outDir: ' + e.message); }
|
162
|
+
|
163
|
+
const summary = { ip, timestamp: now.toISOString(), commands: {} };
|
164
|
+
|
165
|
+
// Nmap
|
166
|
+
const requestedArgs = NMAP_ARGS_STR.trim().split(/\s+/).filter(Boolean);
|
167
|
+
let nmapArgsBase = requestedArgs.slice();
|
168
|
+
const isRoot = (typeof process.getuid === 'function' && process.getuid() === 0);
|
169
|
+
if (!isRoot) { nmapArgsBase = nmapArgsBase.map(a => a === '-sS' ? '-sT' : a); }
|
170
|
+
const nmapArgs = nmapArgsBase.concat([ip]);
|
171
|
+
summary.commands.nmap = { args: nmapArgs.join(' ') };
|
172
|
+
const nmapRes = await runCmd('nmap', nmapArgs);
|
173
|
+
fs.writeFileSync(path.join(outDir, 'nmap.txt'), (nmapRes.stdout || '') + (nmapRes.stderr ? '\n\nSTDERR:\n' + nmapRes.stderr : ''));
|
174
|
+
summary.commands.nmap.result = { ok: nmapRes.ok, path: 'nmap.txt' };
|
175
|
+
|
176
|
+
// Dig
|
136
177
|
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.
|
178
|
+
fs.writeFileSync(path.join(outDir, 'dig.txt'), (digRes.stdout || '') + (digRes.stderr ? '\n\nSTDERR:\n' + digRes.stderr : ''));
|
179
|
+
summary.commands.dig = { ok: digRes.ok, path: 'dig.txt', reverse: (digRes.stdout || '').trim() };
|
139
180
|
|
181
|
+
// Whois
|
140
182
|
const whoisRes = await runCmd('whois', [ip]);
|
141
|
-
fs.writeFileSync(path.join(outDir, 'whois.txt'), whoisRes.stdout + (whoisRes.stderr ? '\n\n' + whoisRes.stderr : ''));
|
142
|
-
summary.
|
183
|
+
fs.writeFileSync(path.join(outDir, 'whois.txt'), (whoisRes.stdout || '') + (whoisRes.stderr ? '\n\nSTDERR:\n' + whoisRes.stderr : ''));
|
184
|
+
summary.commands.whois = { ok: whoisRes.ok, path: 'whois.txt' };
|
185
|
+
|
186
|
+
// Open ports from Nmap
|
187
|
+
try {
|
188
|
+
const openLines = (nmapRes.stdout || '').split(/\r?\n/).filter(l => /^\d+\/tcp\s+open/.test(l)).map(l => l.trim());
|
189
|
+
summary.open_ports = openLines;
|
190
|
+
} catch (e) { summary.open_ports = []; }
|
143
191
|
|
144
192
|
fs.writeFileSync(path.join(outDir, 'summary.json'), JSON.stringify(summary, null, 2));
|
193
|
+
try { fs.chmodSync(outDir, 0o750); } catch (e) {}
|
145
194
|
}
|
146
195
|
|
147
|
-
|
148
|
-
|
196
|
+
// --------- Start daemon ----------
|
197
|
+
const queue = new ScanQueue(CONCURRENCY);
|
198
|
+
log('Fail2Scan daemon started on', LOG_PATH, 'output ->', OUT_ROOT, 'concurrency', CONCURRENCY);
|
199
|
+
|
149
200
|
const tail = new FileTail(LOG_PATH, (line) => {
|
150
|
-
const
|
151
|
-
if (
|
201
|
+
const ip = extractIpFromLine(line);
|
202
|
+
if (ip) queue.push(ip);
|
152
203
|
});
|
153
204
|
|
154
|
-
|
155
|
-
|
205
|
+
// --------- Graceful shutdown ----------
|
206
|
+
function shutdown() {
|
207
|
+
log('Shutting down Fail2Scan daemon...');
|
208
|
+
tail.close();
|
209
|
+
const start = Date.now();
|
210
|
+
const wait = () => {
|
211
|
+
if (queue.running === 0 || Date.now() - start > 10000) process.exit(0);
|
212
|
+
setTimeout(wait, 500);
|
213
|
+
};
|
214
|
+
wait();
|
215
|
+
}
|
216
|
+
process.on('SIGINT', shutdown);
|
217
|
+
process.on('SIGTERM', shutdown);
|