@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.
Files changed (2) hide show
  1. package/bin/daemon.js +132 -70
  2. 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 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
- *
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('Usage: fail2scan-daemon [--log /path/to/fail2ban.log] [--out /path/to/output] [--concurrency N] [--nmap-args "args"] [--quiet]');
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
- async function which(bin) { try { await execFileP('which', [bin]); return true; } catch { return false; } }
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
- async function checkPrereqs() {
62
+ // --------- Prerequisites check ----------
63
+ (async function checkPrereqs() {
56
64
  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);
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.watch = null;
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.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
- }
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
- this.position = st.size;
97
- } catch {}
98
- });
119
+ }
120
+ this.position = st.size;
121
+ } catch (e) {}
99
122
  }
100
123
 
101
- close() { try { if (this.watch) this.watch.close(); } catch {} }
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.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(); });
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 run(ip) {
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
- 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');
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}_${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
-
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.cmds.dig = { ok: digRes.ok };
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.cmds.whois = { ok: whoisRes.ok };
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
- const q = new ScanQueue(CONCURRENCY);
148
- log('Fail2Scan daemon started on', LOG_PATH);
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 match = BAN_RE.exec(line);
151
- if (match && match[1]) q.push(match[1]);
201
+ const ip = extractIpFromLine(line);
202
+ if (ip) queue.push(ip);
152
203
  });
153
204
 
154
- process.on('SIGINT', () => { log('Stopping...'); tail.close(); process.exit(0); });
155
- process.on('SIGTERM', () => { log('Stopping...'); tail.close(); process.exit(0); });
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roflsec/fail2scan",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Fail2Scan daemon - watches fail2ban logs and scans banned IPs using nmap, dig and whois.",
5
5
  "bin": {
6
6
  "fail2scan-daemon": "./bin/daemon.js"