@roflsec/fail2scan 0.0.2 → 0.0.3
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 +258 -129
- package/package.json +1 -1
package/bin/daemon.js
CHANGED
|
@@ -2,23 +2,26 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Fail2Scan
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Fail2Scan daemon - integrated version
|
|
6
|
+
* - watches fail2ban log for "Ban" events (handles rotation)
|
|
7
|
+
* - extracts IPv4/IPv6
|
|
8
|
+
* - queue with persistence and rescan TTL
|
|
9
|
+
* - fallback output dir if /var/log/fail2scan not writable
|
|
10
|
+
* - single-ip CLI --scan-ip
|
|
8
11
|
*
|
|
9
12
|
* Usage:
|
|
10
|
-
* fail2scan-daemon --log /var/log/fail2ban.log --out /var/log/fail2scan --concurrency
|
|
13
|
+
* fail2scan-daemon --log /var/log/fail2ban.log --out /var/log/fail2scan --concurrency 2 --nmap-args "-sS -Pn -p- -T4 -sV" --quiet
|
|
11
14
|
*/
|
|
12
15
|
|
|
13
16
|
const fs = require('fs');
|
|
14
17
|
const path = require('path');
|
|
15
|
-
const
|
|
18
|
+
const os = require('os');
|
|
19
|
+
const { execFile, spawn } = require('child_process');
|
|
16
20
|
const { promisify } = require('util');
|
|
17
|
-
|
|
18
21
|
const execFileP = promisify(execFile);
|
|
19
|
-
const argv = process.argv.slice(2);
|
|
20
22
|
|
|
21
|
-
//
|
|
23
|
+
// -------------------- CLI / CONFIG --------------------
|
|
24
|
+
const argv = process.argv.slice(2);
|
|
22
25
|
function getArg(key, def) {
|
|
23
26
|
for (let i = 0; i < argv.length; i++) {
|
|
24
27
|
const a = argv[i];
|
|
@@ -27,9 +30,8 @@ function getArg(key, def) {
|
|
|
27
30
|
}
|
|
28
31
|
return def;
|
|
29
32
|
}
|
|
30
|
-
|
|
31
33
|
if (argv.includes('--help') || argv.includes('-h')) {
|
|
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');
|
|
34
|
+
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--scan-ip IP (do one scan and exit)\n--quiet');
|
|
33
35
|
process.exit(0);
|
|
34
36
|
}
|
|
35
37
|
|
|
@@ -37,18 +39,22 @@ const LOG_PATH = getArg('--log', '/var/log/fail2ban.log');
|
|
|
37
39
|
const OUT_ROOT = getArg('--out', '/var/log/fail2scan');
|
|
38
40
|
const CONCURRENCY = Math.max(1, parseInt(getArg('--concurrency', '1'), 10) || 1);
|
|
39
41
|
const NMAP_ARGS_STR = getArg('--nmap-args', '-sS -Pn -p- -T4 -sV');
|
|
42
|
+
const SINGLE_IP = getArg('--scan-ip', null);
|
|
40
43
|
const QUIET = argv.includes('--quiet');
|
|
41
44
|
|
|
42
|
-
function log(...args) { if (!QUIET) console.log(new Date().toISOString(), ...args); }
|
|
45
|
+
function log(...args) { if (!QUIET) console.log(new Date().toISOString(), ...args); appendLog(args.join(' ')); }
|
|
43
46
|
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
try {
|
|
47
|
+
// -------------------- small logger to file --------------------
|
|
48
|
+
const STATE_FILE = path.join(os.homedir(), '.fail2scan_state.json');
|
|
49
|
+
const LOG_FILE = path.join(os.homedir(), '.fail2scan.log');
|
|
50
|
+
function appendLog(msg) {
|
|
51
|
+
try { fs.appendFileSync(LOG_FILE, new Date().toISOString() + ' ' + msg + '\n'); } catch (e) {}
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
|
|
54
|
+
// -------------------- utilities --------------------
|
|
55
|
+
function sanitizeFilename(s) { return String(s).replace(/[:\/\\<>?"|* ]+/g, '_'); }
|
|
56
|
+
async function which(bin) { try { await execFileP('which', [bin]); return true; } catch { return false; } }
|
|
57
|
+
async function runCmdCapture(cmd, args, opts = {}) {
|
|
52
58
|
try {
|
|
53
59
|
const { stdout, stderr } = await execFileP(cmd, args, { maxBuffer: 1024 * 1024 * 32, ...opts });
|
|
54
60
|
return { ok: true, stdout: stdout || '', stderr: stderr || '' };
|
|
@@ -56,159 +62,282 @@ async function runCmd(cmd, args, opts = {}) {
|
|
|
56
62
|
return { ok: false, stdout: (e.stdout || '') + '', stderr: (e.stderr || e.message) + '' };
|
|
57
63
|
}
|
|
58
64
|
}
|
|
65
|
+
function safeMkdirSyncWithFallback(p) {
|
|
66
|
+
try { fs.mkdirSync(p, { recursive: true, mode: 0o750 }); return p; }
|
|
67
|
+
catch (e) {
|
|
68
|
+
const fallback = path.join('/tmp', 'fail2scan');
|
|
69
|
+
try { fs.mkdirSync(fallback, { recursive: true, mode: 0o750 }); return fallback; }
|
|
70
|
+
catch (ee) { throw e; }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
59
73
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
(async function checkPrereqs() {
|
|
64
|
-
const tools = ['nmap', 'dig', 'whois'];
|
|
74
|
+
// -------------------- prerequisites --------------------
|
|
75
|
+
(async function checkTools() {
|
|
76
|
+
const tools = ['nmap', 'dig', 'whois', 'which'];
|
|
65
77
|
for (const t of tools) {
|
|
78
|
+
if (t === 'which') continue;
|
|
66
79
|
if (!(await which(t))) {
|
|
67
|
-
console.error(`Missing required binary: ${t}.
|
|
80
|
+
console.error(`Missing required binary: ${t}. Please install it (eg: apt install ${t}).`);
|
|
68
81
|
process.exit(2);
|
|
69
82
|
}
|
|
70
83
|
}
|
|
71
84
|
})().catch(e => { console.error('Prereq check failed', e); process.exit(2); });
|
|
72
85
|
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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() {
|
|
106
|
-
try {
|
|
107
|
-
const st = fs.statSync(this.filePath);
|
|
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);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
this.position = st.size;
|
|
121
|
-
} catch (e) {}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
close() { try { if (this.watcher) this.watcher.close(); } catch (e) {} }
|
|
86
|
+
// -------------------- persistence state --------------------
|
|
87
|
+
function loadState() {
|
|
88
|
+
try {
|
|
89
|
+
if (fs.existsSync(STATE_FILE)) {
|
|
90
|
+
const raw = fs.readFileSync(STATE_FILE, 'utf8');
|
|
91
|
+
const j = JSON.parse(raw);
|
|
92
|
+
return {
|
|
93
|
+
seen: new Set(Array.isArray(j.seen) ? j.seen : []),
|
|
94
|
+
retryAfter: typeof j.retryAfter === 'object' ? j.retryAfter : {}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
} catch (e) {}
|
|
98
|
+
return { seen: new Set(), retryAfter: {} };
|
|
125
99
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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()); });
|
|
140
|
-
}
|
|
141
|
-
async _run(ip) {
|
|
142
|
-
try { log('Scanning', ip); await performScan(ip); log('Done', ip); } catch (e) { console.error('Error scanning', ip, e); }
|
|
143
|
-
}
|
|
100
|
+
function saveState(state) {
|
|
101
|
+
try {
|
|
102
|
+
const obj = { seen: Array.from(state.seen || []), retryAfter: state.retryAfter || {} };
|
|
103
|
+
const dir = path.dirname(STATE_FILE);
|
|
104
|
+
try { fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); } catch (e) {}
|
|
105
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify(obj, null, 2));
|
|
106
|
+
} catch (e) {}
|
|
144
107
|
}
|
|
108
|
+
const STATE = loadState();
|
|
109
|
+
const RESCAN_TTL_SEC = 60 * 60; // default 1 hour
|
|
145
110
|
|
|
111
|
+
// -------------------- IP extraction --------------------
|
|
146
112
|
function extractIpFromLine(line) {
|
|
113
|
+
// prefer strict IPv4
|
|
147
114
|
const ipv4 = line.match(/\b(?:\d{1,3}\.){3}\d{1,3}\b/);
|
|
148
115
|
if (ipv4 && ipv4[0]) return ipv4[0];
|
|
116
|
+
// simple IPv6 match
|
|
149
117
|
const ipv6 = line.match(/\b([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}\b/);
|
|
150
118
|
if (ipv6 && ipv6[0]) return ipv6[0];
|
|
151
119
|
return null;
|
|
152
120
|
}
|
|
153
121
|
|
|
154
|
-
//
|
|
122
|
+
// -------------------- Scan implementation --------------------
|
|
123
|
+
function spawnNmap(ip, outDir, nmapArgs) {
|
|
124
|
+
return new Promise((resolve, reject) => {
|
|
125
|
+
const outNmap = path.join(outDir, 'nmap.txt');
|
|
126
|
+
const args = nmapArgs.concat([ip]);
|
|
127
|
+
const proc = spawn('nmap', args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
128
|
+
const outStream = fs.createWriteStream(outNmap, { flags: 'w' });
|
|
129
|
+
proc.stdout.pipe(outStream);
|
|
130
|
+
let stderr = '';
|
|
131
|
+
proc.stderr.on('data', c => { stderr += c.toString(); });
|
|
132
|
+
proc.on('close', code => {
|
|
133
|
+
if (stderr) fs.appendFileSync(outNmap, '\n\nSTDERR:\n' + stderr);
|
|
134
|
+
resolve({ code, ok: code === 0 });
|
|
135
|
+
});
|
|
136
|
+
proc.on('error', err => reject(err));
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
155
140
|
async function performScan(ip) {
|
|
156
141
|
const now = new Date();
|
|
157
|
-
const iso = now.toISOString().replace(/\..+$/, '').replace(/:/g, '-');
|
|
158
142
|
const dateDir = now.toISOString().slice(0, 10);
|
|
159
143
|
const safeIp = sanitizeFilename(ip);
|
|
160
|
-
|
|
161
|
-
try {
|
|
144
|
+
let outDir = path.join(OUT_ROOT, dateDir, `${safeIp}_${now.toISOString().replace(/[:.]/g, '-')}`);
|
|
145
|
+
try {
|
|
146
|
+
outDir = path.join(safeMkdirSyncWithFallback(path.dirname(outDir)), path.basename(outDir));
|
|
147
|
+
fs.mkdirSync(outDir, { recursive: true, mode: 0o750 });
|
|
148
|
+
} catch (e) {
|
|
149
|
+
log('Cannot create out dir for', ip, '-', e.message);
|
|
150
|
+
// schedule quick retry and exit
|
|
151
|
+
STATE.retryAfter[ip] = Math.floor(Date.now() / 1000) + 60;
|
|
152
|
+
saveState(STATE);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
162
155
|
|
|
163
|
-
const summary = { ip,
|
|
156
|
+
const summary = { ip, ts: now.toISOString(), cmds: {} };
|
|
164
157
|
|
|
165
|
-
//
|
|
166
|
-
const
|
|
167
|
-
let nmapArgsBase = requestedArgs.slice();
|
|
158
|
+
// choose nmap args and adapt if not root
|
|
159
|
+
const requested = NMAP_ARGS_STR.trim().split(/\s+/).filter(Boolean);
|
|
168
160
|
const isRoot = (typeof process.getuid === 'function' && process.getuid() === 0);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
161
|
+
const nmapArgs = requested.map(a => (!isRoot && a === '-sS') ? '-sT' : a);
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
log('Running nmap on', ip, 'args:', nmapArgs.join(' '));
|
|
165
|
+
await spawnNmap(ip, outDir, nmapArgs);
|
|
166
|
+
summary.cmds.nmap = { ok: true, args: nmapArgs.join(' '), path: 'nmap.txt' };
|
|
167
|
+
} catch (e) {
|
|
168
|
+
log('nmap failed for', ip, e.message);
|
|
169
|
+
summary.cmds.nmap = { ok: false, err: e.message };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// dig
|
|
173
|
+
try {
|
|
174
|
+
const dig = await runCmdCapture('dig', ['-x', ip, '+short']);
|
|
175
|
+
fs.writeFileSync(path.join(outDir, 'dig.txt'), (dig.stdout || '') + (dig.stderr ? '\n\nSTDERR:\n' + dig.stderr : ''));
|
|
176
|
+
summary.cmds.dig = { ok: dig.ok, path: 'dig.txt' };
|
|
177
|
+
} catch (e) { summary.cmds.dig = { ok: false, err: e.message }; }
|
|
178
|
+
|
|
179
|
+
// whois
|
|
187
180
|
try {
|
|
188
|
-
const
|
|
189
|
-
|
|
181
|
+
const who = await runCmdCapture('whois', [ip]);
|
|
182
|
+
fs.writeFileSync(path.join(outDir, 'whois.txt'), (who.stdout || '') + (who.stderr ? '\n\nSTDERR:\n' + who.stderr : ''));
|
|
183
|
+
summary.cmds.whois = { ok: who.ok, path: 'whois.txt' };
|
|
184
|
+
} catch (e) { summary.cmds.whois = { ok: false, err: e.message }; }
|
|
185
|
+
|
|
186
|
+
// minimal parse for open ports
|
|
187
|
+
try {
|
|
188
|
+
const nmapTxt = fs.readFileSync(path.join(outDir, 'nmap.txt'), 'utf8');
|
|
189
|
+
const open = nmapTxt.split(/\r?\n/).filter(l => /^\d+\/tcp\s+open/.test(l)).map(l => l.trim());
|
|
190
|
+
summary.open_ports = open;
|
|
190
191
|
} catch (e) { summary.open_ports = []; }
|
|
191
192
|
|
|
192
193
|
fs.writeFileSync(path.join(outDir, 'summary.json'), JSON.stringify(summary, null, 2));
|
|
193
194
|
try { fs.chmodSync(outDir, 0o750); } catch (e) {}
|
|
195
|
+
log('Scan written for', ip, '->', outDir);
|
|
194
196
|
}
|
|
195
197
|
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
|
|
198
|
+
// -------------------- Queue with concurrency and TTL --------------------
|
|
199
|
+
class ScanQueue {
|
|
200
|
+
constructor(concurrency = 1) {
|
|
201
|
+
this.concurrency = concurrency;
|
|
202
|
+
this.running = 0;
|
|
203
|
+
this.q = [];
|
|
204
|
+
this.set = STATE.seen; // persistent set
|
|
205
|
+
}
|
|
206
|
+
push(ip) {
|
|
207
|
+
const now = Math.floor(Date.now() / 1000);
|
|
208
|
+
const next = STATE.retryAfter[ip] || 0;
|
|
209
|
+
if (this.set.has(ip) && next > now) {
|
|
210
|
+
log('IP already queued or running (TTL not expired):', ip);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (this.set.has(ip) && next <= now) {
|
|
214
|
+
log('Re-queueing after TTL:', ip);
|
|
215
|
+
this.set.delete(ip);
|
|
216
|
+
}
|
|
217
|
+
this.set.add(ip);
|
|
218
|
+
STATE.seen = this.set;
|
|
219
|
+
saveState(STATE);
|
|
220
|
+
this.q.push(ip);
|
|
221
|
+
this._next();
|
|
222
|
+
}
|
|
223
|
+
_next() {
|
|
224
|
+
if (this.running >= this.concurrency) return;
|
|
225
|
+
const ip = this.q.shift();
|
|
226
|
+
if (!ip) return;
|
|
227
|
+
this.running++;
|
|
228
|
+
(async () => {
|
|
229
|
+
try {
|
|
230
|
+
log('Scanning', ip);
|
|
231
|
+
await performScan(ip);
|
|
232
|
+
log('Done', ip);
|
|
233
|
+
} catch (e) {
|
|
234
|
+
log('Error scanning', ip, e && e.message ? e.message : e);
|
|
235
|
+
} finally {
|
|
236
|
+
STATE.retryAfter[ip] = Math.floor(Date.now() / 1000) + RESCAN_TTL_SEC;
|
|
237
|
+
saveState(STATE);
|
|
238
|
+
this.set.delete(ip); // allow future re-queue after TTL (state still records retryAfter)
|
|
239
|
+
this.running--;
|
|
240
|
+
setImmediate(() => this._next());
|
|
241
|
+
}
|
|
242
|
+
})();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
199
245
|
|
|
246
|
+
// -------------------- File tail (watch + read new lines, handle rotation) --------------------
|
|
247
|
+
class FileTail {
|
|
248
|
+
constructor(filePath, onLine) {
|
|
249
|
+
this.filePath = filePath;
|
|
250
|
+
this.onLine = onLine;
|
|
251
|
+
this.pos = 0;
|
|
252
|
+
this.inode = null;
|
|
253
|
+
this.buf = '';
|
|
254
|
+
this.watch = null;
|
|
255
|
+
this.start();
|
|
256
|
+
}
|
|
257
|
+
start() {
|
|
258
|
+
try {
|
|
259
|
+
const st = fs.statSync(this.filePath);
|
|
260
|
+
this.inode = st.ino;
|
|
261
|
+
this.pos = st.size;
|
|
262
|
+
} catch (e) {
|
|
263
|
+
this.inode = null;
|
|
264
|
+
this.pos = 0;
|
|
265
|
+
}
|
|
266
|
+
this._watch();
|
|
267
|
+
// try initial read (if file exists and appended)
|
|
268
|
+
this._readNew().catch(()=>{});
|
|
269
|
+
}
|
|
270
|
+
_watch() {
|
|
271
|
+
try {
|
|
272
|
+
this.watch = fs.watch(this.filePath, { persistent: true }, async () => {
|
|
273
|
+
try {
|
|
274
|
+
let st;
|
|
275
|
+
try { st = fs.statSync(this.filePath); } catch { st = null; }
|
|
276
|
+
if (!st) { this.inode = null; this.pos = 0; return; }
|
|
277
|
+
if (this.inode !== null && st.ino !== this.inode) { // rotated
|
|
278
|
+
this.inode = st.ino;
|
|
279
|
+
this.pos = 0;
|
|
280
|
+
} else if (this.inode === null) {
|
|
281
|
+
this.inode = st.ino;
|
|
282
|
+
this.pos = 0;
|
|
283
|
+
}
|
|
284
|
+
await this._readNew();
|
|
285
|
+
} catch (e) { /* ignore transient */ }
|
|
286
|
+
});
|
|
287
|
+
} catch (e) { log('fs.watch failed:', e.message); }
|
|
288
|
+
}
|
|
289
|
+
async _readNew() {
|
|
290
|
+
try {
|
|
291
|
+
const st = fs.statSync(this.filePath);
|
|
292
|
+
if (st.size < this.pos) this.pos = 0;
|
|
293
|
+
if (st.size === this.pos) return;
|
|
294
|
+
const stream = fs.createReadStream(this.filePath, { start: this.pos, end: st.size - 1, encoding: 'utf8' });
|
|
295
|
+
for await (const chunk of stream) {
|
|
296
|
+
this.buf += chunk;
|
|
297
|
+
let idx;
|
|
298
|
+
while ((idx = this.buf.indexOf('\n')) >= 0) {
|
|
299
|
+
const line = this.buf.slice(0, idx);
|
|
300
|
+
this.buf = this.buf.slice(idx + 1);
|
|
301
|
+
if (line.trim()) this.onLine(line);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
this.pos = st.size;
|
|
305
|
+
} catch (e) { /* ignore */ }
|
|
306
|
+
}
|
|
307
|
+
close() { try { if (this.watch) this.watch.close(); } catch (e) {} }
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// -------------------- Main startup --------------------
|
|
311
|
+
if (SINGLE_IP) {
|
|
312
|
+
(async () => {
|
|
313
|
+
const q = new ScanQueue(CONCURRENCY);
|
|
314
|
+
q.push(SINGLE_IP);
|
|
315
|
+
})();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const q = new ScanQueue(CONCURRENCY);
|
|
320
|
+
log('Fail2Scan started. Watching', LOG_PATH, ' -> output', OUT_ROOT, 'concurrency', CONCURRENCY);
|
|
321
|
+
|
|
322
|
+
// On each new line, extract IP and push to queue if Ban detected
|
|
323
|
+
const BAN_RE = /\bBan\b/i;
|
|
200
324
|
const tail = new FileTail(LOG_PATH, (line) => {
|
|
201
|
-
|
|
202
|
-
|
|
325
|
+
try {
|
|
326
|
+
if (!BAN_RE.test(line)) return;
|
|
327
|
+
const ip = extractIpFromLine(line);
|
|
328
|
+
if (!ip) return;
|
|
329
|
+
q.push(ip);
|
|
330
|
+
} catch (e) { log('onLine handler error', e && e.message ? e.message : e); }
|
|
203
331
|
});
|
|
204
332
|
|
|
205
|
-
//
|
|
333
|
+
// graceful shutdown
|
|
206
334
|
function shutdown() {
|
|
207
|
-
log('Shutting down Fail2Scan
|
|
335
|
+
log('Shutting down Fail2Scan...');
|
|
208
336
|
tail.close();
|
|
337
|
+
// wait briefly for running tasks
|
|
209
338
|
const start = Date.now();
|
|
210
339
|
const wait = () => {
|
|
211
|
-
if (
|
|
340
|
+
if (q.running === 0 || Date.now() - start > 10000) process.exit(0);
|
|
212
341
|
setTimeout(wait, 500);
|
|
213
342
|
};
|
|
214
343
|
wait();
|