@roflsec/fail2scan 0.0.13 → 0.0.14

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 +96 -94
  2. package/package.json +1 -1
package/bin/daemon.js CHANGED
@@ -9,9 +9,9 @@ const execFileP = promisify(execFile);
9
9
  // === Géolocalisation ===
10
10
  let getGeo = async (ip) => null;
11
11
  if (process.env.IPGEO_API_KEY) {
12
- const IPGeolocationAPI = require("ip-geolocation-api-javascript-sdk");
13
- const GeolocationParams = require("ip-geolocation-api-javascript-sdk/GeolocationParams.js");
12
+ const { default: IPGeolocationAPI, GeolocationParams } = require("ip-geolocation-api-javascript-sdk");
14
13
  const ipGeo = new IPGeolocationAPI(process.env.IPGEO_API_KEY, true);
14
+
15
15
  getGeo = (ip) => {
16
16
  return new Promise((resolve) => {
17
17
  const params = new GeolocationParams();
@@ -24,8 +24,9 @@ if (process.env.IPGEO_API_KEY) {
24
24
 
25
25
  // === Arguments CLI ===
26
26
  const argv = process.argv.slice(2);
27
- const getArg = (k,d) => { for(let i=0;i<argv.length;i++){const a=argv[i]; if(a===k&&argv[i+1]) return argv[++i]; if(a.startsWith(k+'=')) return a.split('=')[1]; } return d; };
28
- if(argv.includes('--help')||argv.includes('-h')){console.log(`Fail2Scan optimized daemon
27
+ const getArg = (k, d) => { for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a === k && argv[i + 1]) return argv[++i]; if (a.startsWith(k + '=')) return a.split('=')[1]; } return d; };
28
+ if (argv.includes('--help') || argv.includes('-h')) {
29
+ console.log(`Fail2Scan optimized daemon
29
30
  --log PATH (default /var/log/fail2ban.log)
30
31
  --out PATH (default /var/log/fail2scan)
31
32
  --concurrency N (default 1)
@@ -33,38 +34,39 @@ if(argv.includes('--help')||argv.includes('-h')){console.log(`Fail2Scan optimize
33
34
  --nmap-args "args" (default "-sS -Pn -p- -T4 -sV")
34
35
  --scan-ip IP (do one scan and exit)
35
36
  --quiet
36
- `); process.exit(0); }
37
-
38
- const LOG_PATH = getArg('--log','/var/log/fail2ban.log');
39
- const OUT_ROOT = getArg('--out','/var/log/fail2scan');
40
- const USER_CONCURRENCY = parseInt(getArg('--concurrency','0'),10)||0;
41
- const CORE_OVERRIDE = parseInt(getArg('--cores','0'),10)||0;
42
- const NMAP_ARGS_STR = getArg('--nmap-args','-sS -Pn -p- -T4 -sV');
43
- const SINGLE_IP = getArg('--scan-ip',null);
37
+ `); process.exit(0);
38
+ }
39
+
40
+ const LOG_PATH = getArg('--log', '/var/log/fail2ban.log');
41
+ const OUT_ROOT = getArg('--out', '/var/log/fail2scan');
42
+ const USER_CONCURRENCY = parseInt(getArg('--concurrency', '0'), 10) || 0;
43
+ const CORE_OVERRIDE = parseInt(getArg('--cores', '0'), 10) || 0;
44
+ const NMAP_ARGS_STR = getArg('--nmap-args', '-sS -Pn -p- -T4 -sV');
45
+ const SINGLE_IP = getArg('--scan-ip', null);
44
46
  const QUIET = argv.includes('--quiet');
45
- const RESCAN_TTL_SEC = 60*60;
46
- const STATE_FILE = path.join(os.homedir(),'.fail2scan_state.json');
47
- const LOG_FILE = path.join(os.homedir(),'.fail2scan.log');
47
+ const RESCAN_TTL_SEC = 60 * 60;
48
+ const STATE_FILE = path.join(os.homedir(), '.fail2scan_state.json');
49
+ const LOG_FILE = path.join(os.homedir(), '.fail2scan.log');
48
50
 
49
51
  // === Logging ===
50
- const log=(...a)=>{if(!QUIET)console.log(new Date().toISOString(),...a); try{fs.appendFileSync(LOG_FILE,new Date().toISOString()+' '+a.join(' ')+'\n');}catch{}};
52
+ const log = (...a) => { if (!QUIET) console.log(new Date().toISOString(), ...a); try { fs.appendFileSync(LOG_FILE, new Date().toISOString() + ' ' + a.join(' ') + '\n'); } catch { } };
51
53
 
52
54
  // === Utilitaires ===
53
- const sanitizeFilename=s=>String(s).replace(/[:\/\\<>?"|* ]+/g,'_');
54
- async function which(bin){try{await execFileP('which',[bin]);return true;}catch{return false;}}
55
- async function runCmdCapture(cmd,args,opts={}){try{const {stdout,stderr}=await execFileP(cmd,args,{maxBuffer:1024*1024*32,...opts}); return {ok:true,stdout:stdout||'',stderr:stderr||''};}catch(e){return {ok:false,stdout:(e.stdout||'')+'',stderr:(e.stderr||e.message)+''};}}
56
- function safeMkdirSyncWithFallback(p){try{return fs.mkdirSync(p,{recursive:true,mode:0o750})||p}catch(e){const f=path.join('/tmp','fail2scan');try{return fs.mkdirSync(f,{recursive:true,mode:0o750})||f}catch(ee){throw e;}}}
55
+ const sanitizeFilename = s => 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 = {}) { try { const { stdout, stderr } = await execFileP(cmd, args, { maxBuffer: 1024 * 1024 * 32, ...opts }); return { ok: true, stdout: stdout || '', stderr: stderr || '' }; } catch (e) { return { ok: false, stdout: (e.stdout || '') + '', stderr: (e.stderr || e.message) + '' }; } }
58
+ function safeMkdirSyncWithFallback(p) { try { return fs.mkdirSync(p, { recursive: true, mode: 0o750 }) || p } catch (e) { const f = path.join('/tmp', 'fail2scan'); try { return fs.mkdirSync(f, { recursive: true, mode: 0o750 }) || f } catch (ee) { throw e; } } }
57
59
 
58
60
  // === Vérification prérequis ===
59
- (async()=>{for(const t of ['nmap','dig','whois','which']){if(t==='which')continue;if(!(await which(t))){console.error(`Missing required binary: ${t}`);process.exit(2);}}})().catch(e=>{console.error('Prereq check failed',e);process.exit(2);});
61
+ (async () => { for (const t of ['nmap', 'dig', 'whois', 'which']) { if (t === 'which') continue; if (!(await which(t))) { console.error(`Missing required binary: ${t}`); process.exit(2); } } })().catch(e => { console.error('Prereq check failed', e); process.exit(2); });
60
62
 
61
63
  // === Etat ===
62
- function loadState(){try{if(fs.existsSync(STATE_FILE)){const j=JSON.parse(fs.readFileSync(STATE_FILE,'utf8'));return{seen:new Set(Array.isArray(j.seen)?j.seen:[]),retryAfter:typeof j.retryAfter==='object'?j.retryAfter:{}};}}catch{}return{seen:new Set(),retryAfter:{}};}
63
- function saveState(s){try{fs.mkdirSync(path.dirname(STATE_FILE),{recursive:true,mode:0o700});fs.writeFileSync(STATE_FILE,JSON.stringify({seen:Array.from(s.seen||[]),retryAfter:s.retryAfter||{}},null,2));}catch{}}
64
- const STATE=loadState();
64
+ function loadState() { try { if (fs.existsSync(STATE_FILE)) { const j = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')); return { seen: new Set(Array.isArray(j.seen) ? j.seen : []), retryAfter: typeof j.retryAfter === 'object' ? j.retryAfter : {} }; } } catch { } return { seen: new Set(), retryAfter: {} }; }
65
+ function saveState(s) { try { fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true, mode: 0o700 }); fs.writeFileSync(STATE_FILE, JSON.stringify({ seen: Array.from(s.seen || []), retryAfter: s.retryAfter || {} }, null, 2)); } catch { } }
66
+ const STATE = loadState();
65
67
 
66
68
  // === Extraction IP ===
67
- function extractIpFromLine(line){const v4=line.match(/\b(?:\d{1,3}\.){3}\d{1,3}\b/);if(v4&&v4[0])return v4[0];const v6=line.match(/\b([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}\b/);if(v6&&v6[0])return v6[0];return null;}
69
+ function extractIpFromLine(line) { const v4 = line.match(/\b(?:\d{1,3}\.){3}\d{1,3}\b/); if (v4 && v4[0]) return v4[0]; const v6 = line.match(/\b([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}\b/); if (v6 && v6[0]) return v6[0]; return null; }
68
70
 
69
71
  // === Nmap spawn ===
70
72
  function spawnOneNmap(args, outFile) {
@@ -76,7 +78,7 @@ function spawnOneNmap(args, outFile) {
76
78
  proc.stderr.on('data', c => { stderr += c.toString(); });
77
79
  proc.on('close', code => {
78
80
  if (stderr) {
79
- try { fs.appendFileSync(outFile, '\n\nSTDERR:\n' + stderr); } catch (e) {}
81
+ try { fs.appendFileSync(outFile, '\n\nSTDERR:\n' + stderr); } catch (e) { }
80
82
  }
81
83
  resolve({ code, ok: code === 0 });
82
84
  });
@@ -100,7 +102,7 @@ async function spawnNmapParallel(ip, outDir, requestedArgs, parts) {
100
102
  const start = 1 + i * portsPer;
101
103
  const end = (i === numParts - 1) ? 65535 : ((i + 1) * portsPer);
102
104
  const subdir = path.join(outDir, `part-${i}`);
103
- try { fs.mkdirSync(subdir, { recursive: true, mode: 0o750 }); } catch (e) {}
105
+ try { fs.mkdirSync(subdir, { recursive: true, mode: 0o750 }); } catch (e) { }
104
106
  const outNmap = path.join(subdir, 'nmap.txt');
105
107
  const portArg = `-p${start}-${end}`;
106
108
  const args = requestedArgs.map(a => a === '-p-' ? portArg : a).concat([ip]);
@@ -113,10 +115,10 @@ async function spawnNmapParallel(ip, outDir, requestedArgs, parts) {
113
115
  for (let i = 0; i < numParts; i++) {
114
116
  const fn = path.join(outDir, `part-${i}`, 'nmap.txt');
115
117
  try {
116
- if (fs.existsSync(fn)) partsContent.push(fs.readFileSync(fn,'utf8'));
117
- } catch (e) {}
118
+ if (fs.existsSync(fn)) partsContent.push(fs.readFileSync(fn, 'utf8'));
119
+ } catch (e) { }
118
120
  }
119
- try { fs.writeFileSync(path.join(outDir, 'nmap.txt'), partsContent.join('\n\n--- PART ---\n\n')); } catch (e) {}
121
+ try { fs.writeFileSync(path.join(outDir, 'nmap.txt'), partsContent.join('\n\n--- PART ---\n\n')); } catch (e) { }
120
122
  }
121
123
 
122
124
  async function runNmap(ip, outDir, requestedArgs) {
@@ -128,104 +130,104 @@ async function runNmap(ip, outDir, requestedArgs) {
128
130
  }
129
131
 
130
132
  // === Scan d’une IP ===
131
- async function performScan(ip){
132
- const now=new Date(),dateDir=now.toISOString().slice(0,10),safeIp=sanitizeFilename(ip);
133
- let outDir=path.join(OUT_ROOT,dateDir,`${safeIp}_${now.toISOString().replace(/[:.]/g,'-')}`);
134
- outDir=path.join(safeMkdirSyncWithFallback(path.dirname(outDir)),path.basename(outDir));
135
- try { fs.mkdirSync(outDir,{recursive:true,mode:0o750}); } catch (e) {}
136
- const summary={ip,ts:now.toISOString(),cmds:{}};
133
+ async function performScan(ip) {
134
+ const now = new Date(), dateDir = now.toISOString().slice(0, 10), safeIp = sanitizeFilename(ip);
135
+ let outDir = path.join(OUT_ROOT, dateDir, `${safeIp}_${now.toISOString().replace(/[:.]/g, '-')}`);
136
+ outDir = path.join(safeMkdirSyncWithFallback(path.dirname(outDir)), path.basename(outDir));
137
+ try { fs.mkdirSync(outDir, { recursive: true, mode: 0o750 }); } catch (e) { }
138
+ const summary = { ip, ts: now.toISOString(), cmds: {} };
137
139
 
138
- const requested=NMAP_ARGS_STR.trim().split(/\s+/).filter(Boolean);
140
+ const requested = NMAP_ARGS_STR.trim().split(/\s+/).filter(Boolean);
139
141
 
140
- try{
141
- log('Running nmap on',ip,'args:',requested.join(' '));
142
+ try {
143
+ log('Running nmap on', ip, 'args:', requested.join(' '));
142
144
  await runNmap(ip, outDir, requested);
143
145
  summary.cmds.nmap = { ok: true, args: requested.join(' '), path: 'nmap.txt' };
144
- } catch(e){
145
- log('nmap failed for',ip, e && e.message ? e.message : e);
146
+ } catch (e) {
147
+ log('nmap failed for', ip, e && e.message ? e.message : e);
146
148
  summary.cmds.nmap = { ok: false, err: e && e.message ? e.message : String(e) };
147
149
  }
148
150
 
149
- try{
150
- const dig = await runCmdCapture('dig',['-x',ip,'+short']);
151
- fs.writeFileSync(path.join(outDir,'dig.txt'),(dig.stdout||'') + (dig.stderr?'\n\nSTDERR:\n'+dig.stderr:''));
152
- summary.cmds.dig = { ok: dig.ok, path: 'dig.txt' };
153
- } catch(e){ summary.cmds.dig = { ok:false, err: e && e.message ? e.message : String(e) }; }
151
+ try {
152
+ const dig = await runCmdCapture('dig', ['-x', ip, '+short']);
153
+ fs.writeFileSync(path.join(outDir, 'dig.txt'), (dig.stdout || '') + (dig.stderr ? '\n\nSTDERR:\n' + dig.stderr : ''));
154
+ summary.cmds.dig = { ok: dig.ok, path: 'dig.txt' };
155
+ } catch (e) { summary.cmds.dig = { ok: false, err: e && e.message ? e.message : String(e) }; }
154
156
 
155
- try{
156
- const who = await runCmdCapture('whois',[ip]);
157
- fs.writeFileSync(path.join(outDir,'whois.txt'),(who.stdout||'') + (who.stderr?'\n\nSTDERR:\n'+who.stderr:''));
158
- summary.cmds.whois = { ok: who.ok, path: 'whois.txt' };
159
- } catch(e){ summary.cmds.whois = { ok:false, err: e && e.message ? e.message : String(e) }; }
157
+ try {
158
+ const who = await runCmdCapture('whois', [ip]);
159
+ fs.writeFileSync(path.join(outDir, 'whois.txt'), (who.stdout || '') + (who.stderr ? '\n\nSTDERR:\n' + who.stderr : ''));
160
+ summary.cmds.whois = { ok: who.ok, path: 'whois.txt' };
161
+ } catch (e) { summary.cmds.whois = { ok: false, err: e && e.message ? e.message : String(e) }; }
160
162
 
161
- try{
162
- const nmapTxt = fs.readFileSync(path.join(outDir,'nmap.txt'),'utf8');
163
- summary.open_ports = nmapTxt.split(/\r?\n/).filter(l=>/^\d+\/tcp\s+open/.test(l)).map(l=>l.trim());
164
- } catch(e){ summary.open_ports = []; }
163
+ try {
164
+ const nmapTxt = fs.readFileSync(path.join(outDir, 'nmap.txt'), 'utf8');
165
+ summary.open_ports = nmapTxt.split(/\r?\n/).filter(l => /^\d+\/tcp\s+open/.test(l)).map(l => l.trim());
166
+ } catch (e) { summary.open_ports = []; }
165
167
 
166
168
  // === Géolocalisation ===
167
- if(getGeo){
168
- try{
169
+ if (getGeo) {
170
+ try {
169
171
  const geo = await getGeo(ip);
170
- fs.writeFileSync(path.join(outDir,'geo.json'), JSON.stringify(geo,null,2));
172
+ fs.writeFileSync(path.join(outDir, 'geo.json'), JSON.stringify(geo, null, 2));
171
173
  summary.geo = geo;
172
- }catch(e){
173
- log('Geo lookup failed for',ip,e.message||e);
174
+ } catch (e) {
175
+ log('Geo lookup failed for', ip, e.message || e);
174
176
  summary.geo = null;
175
177
  }
176
178
  }
177
179
 
178
- try{ fs.writeFileSync(path.join(outDir,'summary.json'),JSON.stringify(summary,null,2)); } catch (e) {}
179
- try{ fs.chmodSync(outDir,0o750); } catch (e) {}
180
- log('Scan written for',ip,'->',outDir);
180
+ try { fs.writeFileSync(path.join(outDir, 'summary.json'), JSON.stringify(summary, null, 2)); } catch (e) { }
181
+ try { fs.chmodSync(outDir, 0o750); } catch (e) { }
182
+ log('Scan written for', ip, '->', outDir);
181
183
  }
182
184
 
183
185
  // === Queue ===
184
- class ScanQueue{
185
- constructor(concurrency=1){this.concurrency=concurrency;this.running=0;this.q=[];this.set=STATE.seen;this.tmpCache=new Set();}
186
- push(ip){
187
- const now=Math.floor(Date.now()/1000),next=STATE.retryAfter[ip]||0;
188
- if((this.set.has(ip)&&next>now)||this.tmpCache.has(ip)){log('IP already queued or running (TTL/cache):',ip);return;}
189
- if(this.set.has(ip)&&next<=now)log('Re-queueing after TTL:',ip),this.set.delete(ip);
190
- this.set.add(ip);STATE.seen=this.set;saveState(STATE);
191
- this.q.push(ip);this.tmpCache.add(ip);this._next();
186
+ class ScanQueue {
187
+ constructor(concurrency = 1) { this.concurrency = concurrency; this.running = 0; this.q = []; this.set = STATE.seen; this.tmpCache = new Set(); }
188
+ push(ip) {
189
+ const now = Math.floor(Date.now() / 1000), next = STATE.retryAfter[ip] || 0;
190
+ if ((this.set.has(ip) && next > now) || this.tmpCache.has(ip)) { log('IP already queued or running (TTL/cache):', ip); return; }
191
+ if (this.set.has(ip) && next <= now) log('Re-queueing after TTL:', ip), this.set.delete(ip);
192
+ this.set.add(ip); STATE.seen = this.set; saveState(STATE);
193
+ this.q.push(ip); this.tmpCache.add(ip); this._next();
192
194
  }
193
- _next(){
194
- if(this.running>=this.concurrency)return;
195
- const ip=this.q.shift();if(!ip)return;
195
+ _next() {
196
+ if (this.running >= this.concurrency) return;
197
+ const ip = this.q.shift(); if (!ip) return;
196
198
  this.running++;
197
- (async()=>{
198
- try{log('Scanning',ip);await performScan(ip);log('Done',ip);}
199
- catch(e){log('Error scanning',ip,e.message||e);}
200
- finally{
201
- STATE.retryAfter[ip]=Math.floor(Date.now()/1000)+RESCAN_TTL_SEC;
199
+ (async () => {
200
+ try { log('Scanning', ip); await performScan(ip); log('Done', ip); }
201
+ catch (e) { log('Error scanning', ip, e.message || e); }
202
+ finally {
203
+ STATE.retryAfter[ip] = Math.floor(Date.now() / 1000) + RESCAN_TTL_SEC;
202
204
  saveState(STATE);
203
- this.set.delete(ip);this.tmpCache.delete(ip);
204
- this.running--;setImmediate(()=>this._next());
205
+ this.set.delete(ip); this.tmpCache.delete(ip);
206
+ this.running--; setImmediate(() => this._next());
205
207
  }
206
208
  })();
207
- setImmediate(()=>this._next());
209
+ setImmediate(() => this._next());
208
210
  }
209
211
  }
210
212
 
211
213
  // === Single IP mode ===
212
- if(SINGLE_IP){(async()=>{const q=new ScanQueue(CORE_OVERRIDE||USER_CONCURRENCY||1);q.push(SINGLE_IP);})();return;}
214
+ if (SINGLE_IP) { (async () => { const q = new ScanQueue(CORE_OVERRIDE || USER_CONCURRENCY || 1); q.push(SINGLE_IP); })(); return; }
213
215
 
214
216
  // === Lancement daemon ===
215
- const concurrency = CORE_OVERRIDE||USER_CONCURRENCY||os.cpus().length||1;
217
+ const concurrency = CORE_OVERRIDE || USER_CONCURRENCY || os.cpus().length || 1;
216
218
  const q = new ScanQueue(concurrency);
217
219
  log(`Fail2Scan started. Watching ${LOG_PATH} -> output ${OUT_ROOT}, concurrency ${concurrency}`);
218
220
 
219
221
  const BAN_RE = /\bBan\b/i;
220
- class FileTail{
221
- constructor(filePath,onLine){this.filePath=filePath;this.onLine=onLine;this.pos=0;this.inode=null;this.buf='';this.watch=null;this.start();}
222
- start(){try{const st=fs.statSync(this.filePath);this.inode=st.ino;this.pos=st.size;}catch{this.inode=null;this.pos=0;}this._watch();this._readNew().catch(()=>{});}
223
- _watch(){try{this.watch=fs.watch(this.filePath,{persistent:true},async()=>{try{const st=fs.statSync(this.filePath);if(!st){this.inode=null;this.pos=0;return;}if(this.inode!==null&&st.ino!==this.inode)this.inode=st.ino,this.pos=0;else if(this.inode===null)this.inode=st.ino,this.pos=0;await this._readNew();}catch{}});}catch(e){log('fs.watch failed:',e.message);}}
224
- async _readNew(){try{const st=fs.statSync(this.filePath);if(st.size<this.pos)this.pos=0;if(st.size===this.pos)return;const stream=fs.createReadStream(this.filePath,{start:this.pos,end:st.size-1,encoding:'utf8'});for await(const chunk of stream){this.buf+=chunk;let idx;while((idx=this.buf.indexOf('\n'))>=0){const line=this.buf.slice(0,idx);this.buf=this.buf.slice(idx+1);if(line.trim())this.onLine(line);}}this.pos=st.size;}catch{}}
225
- close(){try{this.watch?.close();}catch{}}
222
+ class FileTail {
223
+ constructor(filePath, onLine) { this.filePath = filePath; this.onLine = onLine; this.pos = 0; this.inode = null; this.buf = ''; this.watch = null; this.start(); }
224
+ start() { try { const st = fs.statSync(this.filePath); this.inode = st.ino; this.pos = st.size; } catch { this.inode = null; this.pos = 0; } this._watch(); this._readNew().catch(() => { }); }
225
+ _watch() { try { this.watch = fs.watch(this.filePath, { persistent: true }, async () => { try { const st = fs.statSync(this.filePath); if (!st) { this.inode = null; this.pos = 0; return; } if (this.inode !== null && st.ino !== this.inode) this.inode = st.ino, this.pos = 0; else if (this.inode === null) this.inode = st.ino, this.pos = 0; await this._readNew(); } catch { } }); } catch (e) { log('fs.watch failed:', e.message); } }
226
+ async _readNew() { try { const st = fs.statSync(this.filePath); if (st.size < this.pos) this.pos = 0; if (st.size === this.pos) return; const stream = fs.createReadStream(this.filePath, { start: this.pos, end: st.size - 1, encoding: 'utf8' }); for await (const chunk of stream) { this.buf += chunk; let idx; while ((idx = this.buf.indexOf('\n')) >= 0) { const line = this.buf.slice(0, idx); this.buf = this.buf.slice(idx + 1); if (line.trim()) this.onLine(line); } } this.pos = st.size; } catch { } }
227
+ close() { try { this.watch?.close(); } catch { } }
226
228
  }
227
- const tail=new FileTail(LOG_PATH,line=>{try{if(!BAN_RE.test(line))return;const ip=extractIpFromLine(line);if(!ip)return;q.push(ip);}catch(e){log('onLine handler error',e.message||e);}});
229
+ const tail = new FileTail(LOG_PATH, line => { try { if (!BAN_RE.test(line)) return; const ip = extractIpFromLine(line); if (!ip) return; q.push(ip); } catch (e) { log('onLine handler error', e.message || e); } });
228
230
 
229
- function shutdown(){log('Shutting down Fail2Scan...');tail.close();const start=Date.now();const wait=()=>{if(q.running===0||Date.now()-start>10000)process.exit(0);setTimeout(wait,500);};wait();}
230
- process.on('SIGINT',shutdown);
231
- process.on('SIGTERM',shutdown);
231
+ function shutdown() { log('Shutting down Fail2Scan...'); tail.close(); const start = Date.now(); const wait = () => { if (q.running === 0 || Date.now() - start > 10000) process.exit(0); setTimeout(wait, 500); }; wait(); }
232
+ process.on('SIGINT', shutdown);
233
+ process.on('SIGTERM', shutdown);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roflsec/fail2scan",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
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"