@roflsec/fail2scan 0.0.18 → 0.0.20

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 +286 -63
  2. package/package.json +2 -3
package/bin/daemon.js CHANGED
@@ -1,29 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
- require('dotenv').config();
4
3
  const fs = require('fs'), path = require('path'), os = require('os');
5
4
  const { execFile, spawn } = require('child_process');
6
5
  const { promisify } = require('util');
6
+ const https = require('https');
7
7
  const execFileP = promisify(execFile);
8
-
9
- let getGeo = async (ip) => null;
10
- if (process.env.IPGEO_API_KEY) {
11
- const IPGeolocationAPI = require('ip-geolocation-api-javascript-sdk');
12
- const { GeolocationParams } = require('ip-geolocation-api-javascript-sdk');
13
- const ipGeo = new IPGeolocationAPI(process.env.IPGEO_API_KEY, true);
14
- getGeo = (ip) => {
15
- return new Promise((resolve) => {
16
- const params = new GeolocationParams();
17
- params.setIPAddress(ip);
18
- params.setFields("geo,time_zone,currency,asn,security");
19
- ipGeo.getGeolocation((res) => resolve(res), params);
20
- });
21
- };
22
- }
23
-
8
+ require('dotenv').config()
9
+ // -------------------- CLI / CONFIG --------------------
24
10
  const argv = process.argv.slice(2);
25
11
  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; };
26
- if(argv.includes('--help')||argv.includes('-h')){console.log(`Fail2Scan optimized daemon
12
+
13
+ if(argv.includes('--help')||argv.includes('-h')){
14
+ console.log(`Fail2Scan optimized daemon
27
15
  --log PATH (default /var/log/fail2ban.log)
28
16
  --out PATH (default /var/log/fail2scan)
29
17
  --concurrency N (default 1)
@@ -31,7 +19,9 @@ if(argv.includes('--help')||argv.includes('-h')){console.log(`Fail2Scan optimize
31
19
  --nmap-args "args" (default "-sS -Pn -p- -T4 -sV")
32
20
  --scan-ip IP (do one scan and exit)
33
21
  --quiet
34
- `); process.exit(0); }
22
+ `);
23
+ process.exit(0);
24
+ }
35
25
 
36
26
  const LOG_PATH = getArg('--log','/var/log/fail2ban.log');
37
27
  const OUT_ROOT = getArg('--out','/var/log/fail2scan');
@@ -44,21 +34,107 @@ const RESCAN_TTL_SEC = 60*60;
44
34
  const STATE_FILE = path.join(os.homedir(),'.fail2scan_state.json');
45
35
  const LOG_FILE = path.join(os.homedir(),'.fail2scan.log');
46
36
 
47
- const log=(...a)=>{if(!QUIET)console.log(new Date().toISOString(),...a); try{fs.appendFileSync(LOG_FILE,new Date().toISOString()+' '+a.join(' ')+'\n');}catch{}};
37
+ const log=(...a)=>{
38
+ if(!QUIET) console.log(new Date().toISOString(),...a);
39
+ try{ fs.appendFileSync(LOG_FILE,new Date().toISOString()+' '+a.join(' ')+'\n'); }catch{}
40
+ };
48
41
 
42
+ // -------------------- utilities --------------------
49
43
  const sanitizeFilename=s=>String(s).replace(/[:\/\\<>?"|* ]+/g,'_');
50
- async function which(bin){try{await execFileP('which',[bin]);return true;}catch{return false;}}
51
- 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)+''};}}
52
- 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;}}}
53
44
 
54
- (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);});
45
+ async function which(bin){
46
+ try{ await execFileP('which',[bin]); return true; }
47
+ catch{ return false; }
48
+ }
49
+
50
+ async function runCmdCapture(cmd,args,opts={}){
51
+ try{
52
+ const {stdout,stderr}=await execFileP(cmd,args,{maxBuffer:1024*1024*32,...opts});
53
+ return {ok:true,stdout:stdout||'',stderr:stderr||''};
54
+ }catch(e){
55
+ return {ok:false,stdout:(e.stdout||'')+'',stderr:(e.stderr||e.message)+''};
56
+ }
57
+ }
58
+
59
+ function safeMkdirSyncWithFallback(p){
60
+ try{ return fs.mkdirSync(p,{recursive:true,mode:0o750})||p; }
61
+ catch(e){
62
+ const f=path.join('/tmp','fail2scan');
63
+ try{ return fs.mkdirSync(f,{recursive:true,mode:0o750})||f; }
64
+ catch(ee){ throw e; }
65
+ }
66
+ }
67
+
68
+ // -------------------- prerequisites --------------------
69
+ (async()=>{
70
+ for(const t of ['nmap','dig','whois','which']){
71
+ if(t==='which') continue;
72
+ if(!(await which(t))){
73
+ console.error(`Missing required binary: ${t}`);
74
+ process.exit(2);
75
+ }
76
+ }
77
+ })().catch(e=>{
78
+ console.error('Prereq check failed',e);
79
+ process.exit(2);
80
+ });
81
+
82
+ // -------------------- state --------------------
83
+ function loadState(){
84
+ try{
85
+ if(fs.existsSync(STATE_FILE)){
86
+ const j=JSON.parse(fs.readFileSync(STATE_FILE,'utf8'));
87
+ return {
88
+ seen:new Set(Array.isArray(j.seen)?j.seen:[]),
89
+ retryAfter:typeof j.retryAfter==='object'?j.retryAfter:{}
90
+ };
91
+ }
92
+ }catch{}
93
+ return {seen:new Set(),retryAfter:{}};
94
+ }
95
+
96
+ function saveState(s){
97
+ try{
98
+ fs.mkdirSync(path.dirname(STATE_FILE),{recursive:true,mode:0o700});
99
+ fs.writeFileSync(STATE_FILE,JSON.stringify({
100
+ seen:Array.from(s.seen||[]),
101
+ retryAfter:s.retryAfter||{}
102
+ },null,2));
103
+ }catch{}
104
+ }
55
105
 
56
- 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:{}};}
57
- 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{}}
58
106
  const STATE=loadState();
59
107
 
60
- 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;}
108
+ // -------------------- IP extraction --------------------
109
+ function extractIpFromLine(line){
110
+ const v4=line.match(/\b(?:\d{1,3}\.){3}\d{1,3}\b/);
111
+ if(v4&&v4[0]) return v4[0];
112
+ const v6=line.match(/\b([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}\b/);
113
+ if(v6&&v6[0]) return v6[0];
114
+ return null;
115
+ }
116
+
117
+ // -------------------- GEOLOCATION (Native HTTPS) --------------------
118
+ function fetchGeo(ip){
119
+ return new Promise((resolve,reject)=>{
120
+ const apiKey = process.env.IPGEO_API_KEY;
121
+ if(!apiKey) return resolve({ error:"Missing IPGEO_API_KEY" });
61
122
 
123
+ const fields = "geo,time_zone,currency,asn,security";
124
+ const url = `https://api.ipgeolocation.io/ipgeo?apiKey=${apiKey}&ip=${ip}&fields=${fields}`;
125
+
126
+ https.get(url,res=>{
127
+ let data='';
128
+ res.on('data',chunk=>data+=chunk);
129
+ res.on('end',()=>{
130
+ try{ resolve(JSON.parse(data)); }
131
+ catch(e){ resolve({error:"Invalid JSON",raw:data}); }
132
+ });
133
+ }).on('error',reject);
134
+ });
135
+ }
136
+
137
+ // -------------------- nmap helpers --------------------
62
138
  function spawnOneNmap(args, outFile) {
63
139
  return new Promise((resolve, reject) => {
64
140
  const proc = spawn('nmap', args, { stdio: ['ignore', 'pipe', 'pipe'] });
@@ -85,8 +161,8 @@ async function spawnNmapParallel(ip, outDir, requestedArgs, parts) {
85
161
 
86
162
  const cpuCount = os.cpus() ? os.cpus().length : 1;
87
163
  const numParts = Math.max(1, parts || CORE_OVERRIDE || cpuCount);
88
-
89
164
  const portsPer = Math.floor(65535 / numParts);
165
+
90
166
  const jobs = [];
91
167
  for (let i = 0; i < numParts; i++) {
92
168
  const start = 1 + i * portsPer;
@@ -98,14 +174,13 @@ async function spawnNmapParallel(ip, outDir, requestedArgs, parts) {
98
174
  const args = requestedArgs.map(a => a === '-p-' ? portArg : a).concat([ip]);
99
175
  jobs.push(spawnOneNmap(args, outNmap));
100
176
  }
101
-
102
177
  await Promise.all(jobs);
103
178
 
104
179
  const partsContent = [];
105
180
  for (let i = 0; i < numParts; i++) {
106
181
  const fn = path.join(outDir, `part-${i}`, 'nmap.txt');
107
182
  try {
108
- if (fs.existsSync(fn)) partsContent.push(fs.readFileSync(fn,'utf8'));
183
+ if (fs.existsSync(fn)) partsContent.push(fs.readFileSync(fn, 'utf8'));
109
184
  } catch (e) {}
110
185
  }
111
186
  try { fs.writeFileSync(path.join(outDir, 'nmap.txt'), partsContent.join('\n\n--- PART ---\n\n')); } catch (e) {}
@@ -119,17 +194,23 @@ async function runNmap(ip, outDir, requestedArgs) {
119
194
  await spawnNmapParallel(ip, outDir, args, parts);
120
195
  }
121
196
 
197
+ // -------------------- scan --------------------
122
198
  async function performScan(ip){
123
- const now=new Date(),dateDir=now.toISOString().slice(0,10),safeIp=sanitizeFilename(ip);
199
+ const now=new Date(),
200
+ dateDir=now.toISOString().slice(0,10),
201
+ safeIp=sanitizeFilename(ip);
202
+
124
203
  let outDir=path.join(OUT_ROOT,dateDir,`${safeIp}_${now.toISOString().replace(/[:.]/g,'-')}`);
125
204
  outDir=path.join(safeMkdirSyncWithFallback(path.dirname(outDir)),path.basename(outDir));
126
205
  try { fs.mkdirSync(outDir,{recursive:true,mode:0o750}); } catch (e) {}
206
+
127
207
  const summary={ip,ts:now.toISOString(),cmds:{}};
128
208
 
129
209
  const requested=NMAP_ARGS_STR.trim().split(/\s+/).filter(Boolean);
130
210
 
211
+ log('Running nmap on',ip,'args:',requested.join(' '));
212
+
131
213
  try{
132
- log('Running nmap on',ip,'args:',requested.join(' '));
133
214
  await runNmap(ip, outDir, requested);
134
215
  summary.cmds.nmap = { ok: true, args: requested.join(' '), path: 'nmap.txt' };
135
216
  } catch(e){
@@ -137,66 +218,208 @@ async function performScan(ip){
137
218
  summary.cmds.nmap = { ok: false, err: e && e.message ? e.message : String(e) };
138
219
  }
139
220
 
140
- try{ const dig = await runCmdCapture('dig',['-x',ip,'+short']); fs.writeFileSync(path.join(outDir,'dig.txt'),(dig.stdout||'') + (dig.stderr?'\n\nSTDERR:\n'+dig.stderr:'')); summary.cmds.dig = { ok: dig.ok, path: 'dig.txt' }; } catch(e){ summary.cmds.dig = { ok:false, err: e && e.message ? e.message : String(e) }; }
141
- try{ const who = await runCmdCapture('whois',[ip]); fs.writeFileSync(path.join(outDir,'whois.txt'),(who.stdout||'') + (who.stderr?'\n\nSTDERR:\n'+who.stderr:'')); summary.cmds.whois = { ok: who.ok, path: 'whois.txt' }; } catch(e){ summary.cmds.whois = { ok:false, err: e && e.message ? e.message : String(e) }; }
221
+ try{
222
+ const dig = await runCmdCapture('dig',['-x',ip,'+short']);
223
+ fs.writeFileSync(path.join(outDir,'dig.txt'),
224
+ (dig.stdout||'') + (dig.stderr?'\n\nSTDERR:\n'+dig.stderr:'')
225
+ );
226
+ summary.cmds.dig = { ok: dig.ok, path: 'dig.txt' };
227
+ } catch(e){
228
+ summary.cmds.dig = { ok:false, err: e.message||String(e) };
229
+ }
230
+
231
+ try{
232
+ const who = await runCmdCapture('whois',[ip]);
233
+ fs.writeFileSync(path.join(outDir,'whois.txt'),
234
+ (who.stdout||'') + (who.stderr?'\n\nSTDERR:\n'+who.stderr:'')
235
+ );
236
+ summary.cmds.whois = { ok: who.ok, path: 'whois.txt' };
237
+ } catch(e){
238
+ summary.cmds.whois = { ok:false, err:e.message||String(e) };
239
+ }
142
240
 
143
- // Geolocation
241
+ // ---- GEOLOCATION ----
144
242
  try {
145
- const geo = await getGeo(ip);
146
- if (geo) fs.writeFileSync(path.join(outDir,'geo.json'),JSON.stringify(geo,null,2));
147
- summary.cmds.geo = { ok: !!geo, path: 'geo.json' };
148
- } catch(e){ summary.cmds.geo = { ok:false, err: e && e.message ? e.message : String(e) }; }
243
+ const geo = await fetchGeo(ip);
244
+ fs.writeFileSync(path.join(outDir,'geo.json'), JSON.stringify(geo,null,2));
245
+ summary.cmds.geo = { ok:true, path:"geo.json" };
246
+ } catch(e){
247
+ summary.cmds.geo = { ok:false, err:e.message||String(e) };
248
+ }
249
+
250
+ // open ports
251
+ try{
252
+ const nmapTxt = fs.readFileSync(path.join(outDir,'nmap.txt'),'utf8');
253
+ summary.open_ports = nmapTxt
254
+ .split(/\r?\n/)
255
+ .filter(l=>/^\d+\/tcp\s+open/.test(l))
256
+ .map(l=>l.trim());
257
+ } catch(e){
258
+ summary.open_ports = [];
259
+ }
149
260
 
150
- try{ const nmapTxt = fs.readFileSync(path.join(outDir,'nmap.txt'),'utf8'); summary.open_ports = nmapTxt.split(/\r?\n/).filter(l=>/^\d+\/tcp\s+open/.test(l)).map(l=>l.trim()); } catch(e){ summary.open_ports = []; }
261
+ try{ fs.writeFileSync(path.join(outDir,'summary.json'),JSON.stringify(summary,null,2)); }catch{}
262
+ try{ fs.chmodSync(outDir,0o750); }catch{}
151
263
 
152
- try{ fs.writeFileSync(path.join(outDir,'summary.json'),JSON.stringify(summary,null,2)); } catch (e) {}
153
- try{ fs.chmodSync(outDir,0o750); } catch (e) {}
154
264
  log('Scan written for',ip,'->',outDir);
155
265
  }
156
266
 
267
+ // -------------------- queue optimized --------------------
157
268
  class ScanQueue{
158
- constructor(concurrency=1){this.concurrency=concurrency;this.running=0;this.q=[];this.set=STATE.seen;this.tmpCache=new Set();}
269
+ constructor(concurrency=1){
270
+ this.concurrency=concurrency;
271
+ this.running=0;
272
+ this.q=[];
273
+ this.set=STATE.seen;
274
+ this.tmpCache=new Set();
275
+ }
159
276
  push(ip){
160
- const now=Math.floor(Date.now()/1000),next=STATE.retryAfter[ip]||0;
161
- if((this.set.has(ip)&&next>now)||this.tmpCache.has(ip)){log('IP already queued or running (TTL/cache):',ip);return;}
162
- if(this.set.has(ip)&&next<=now)log('Re-queueing after TTL:',ip),this.set.delete(ip);
163
- this.set.add(ip);STATE.seen=this.set;saveState(STATE);
164
- this.q.push(ip);this.tmpCache.add(ip);this._next();
277
+ const now=Math.floor(Date.now()/1000),
278
+ next=STATE.retryAfter[ip]||0;
279
+ if((this.set.has(ip)&&next>now)||this.tmpCache.has(ip)){
280
+ log('IP already queued or running (TTL/cache):',ip);
281
+ return;
282
+ }
283
+ if(this.set.has(ip)&&next<=now){
284
+ log('Re-queueing after TTL:',ip);
285
+ this.set.delete(ip);
286
+ }
287
+ this.set.add(ip);
288
+ STATE.seen=this.set;
289
+ saveState(STATE);
290
+ this.q.push(ip);
291
+ this.tmpCache.add(ip);
292
+ this._next();
165
293
  }
166
294
  _next(){
167
295
  if(this.running>=this.concurrency)return;
168
- const ip=this.q.shift();if(!ip)return;
296
+ const ip=this.q.shift(); if(!ip) return;
169
297
  this.running++;
170
298
  (async()=>{
171
- try{log('Scanning',ip);await performScan(ip);log('Done',ip);}
172
- catch(e){log('Error scanning',ip,e.message||e);}
173
- finally{
299
+ try{
300
+ log('Scanning',ip);
301
+ await performScan(ip);
302
+ log('Done',ip);
303
+ }catch(e){
304
+ log('Error scanning',ip,e.message||e);
305
+ }finally{
174
306
  STATE.retryAfter[ip]=Math.floor(Date.now()/1000)+RESCAN_TTL_SEC;
175
307
  saveState(STATE);
176
- this.set.delete(ip);this.tmpCache.delete(ip);
177
- this.running--;setImmediate(()=>this._next());
308
+ this.set.delete(ip);
309
+ this.tmpCache.delete(ip);
310
+ this.running--;
311
+ setImmediate(()=>this._next());
178
312
  }
179
313
  })();
180
314
  setImmediate(()=>this._next());
181
315
  }
182
316
  }
183
317
 
184
- if(SINGLE_IP){(async()=>{const q=new ScanQueue(CORE_OVERRIDE||USER_CONCURRENCY||1);q.push(SINGLE_IP);})();return;}
318
+ // -------------------- main --------------------
319
+ if(SINGLE_IP){
320
+ (async()=>{
321
+ const q=new ScanQueue(CORE_OVERRIDE||USER_CONCURRENCY||1);
322
+ q.push(SINGLE_IP);
323
+ })();
324
+ return;
325
+ }
326
+
185
327
  const concurrency = CORE_OVERRIDE||USER_CONCURRENCY||os.cpus().length||1;
186
328
  const q = new ScanQueue(concurrency);
187
329
  log(`Fail2Scan started. Watching ${LOG_PATH} -> output ${OUT_ROOT}, concurrency ${concurrency}`);
188
330
 
189
331
  const BAN_RE = /\bBan\b/i;
332
+
190
333
  class FileTail{
191
- constructor(filePath,onLine){this.filePath=filePath;this.onLine=onLine;this.pos=0;this.inode=null;this.buf='';this.watch=null;this.start();}
192
- 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(()=>{});}
193
- _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);}}
194
- 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{}}
195
- close(){try{this.watch?.close();}catch{}}
334
+ constructor(filePath,onLine){
335
+ this.filePath=filePath;
336
+ this.onLine=onLine;
337
+ this.pos=0;
338
+ this.inode=null;
339
+ this.buf='';
340
+ this.watch=null;
341
+ this.start();
342
+ }
343
+ start(){
344
+ try{
345
+ const st=fs.statSync(this.filePath);
346
+ this.inode=st.ino;
347
+ this.pos=st.size;
348
+ }catch{
349
+ this.inode=null;
350
+ this.pos=0;
351
+ }
352
+ this._watch();
353
+ this._readNew().catch(()=>{});
354
+ }
355
+ _watch(){
356
+ try{
357
+ this.watch=fs.watch(this.filePath,{persistent:true},async()=>{
358
+ try{
359
+ const st=fs.statSync(this.filePath);
360
+ if(!st){ this.inode=null; this.pos=0; return; }
361
+ if(this.inode!==null && st.ino!==this.inode){
362
+ this.inode=st.ino; this.pos=0;
363
+ }else if(this.inode===null){
364
+ this.inode=st.ino; this.pos=0;
365
+ }
366
+ await this._readNew();
367
+ }catch{}
368
+ });
369
+ }catch(e){
370
+ log('fs.watch failed:',e.message);
371
+ }
372
+ }
373
+ async _readNew(){
374
+ try{
375
+ const st=fs.statSync(this.filePath);
376
+ if(st.size<this.pos) this.pos=0;
377
+ if(st.size===this.pos) return;
378
+
379
+ const stream=fs.createReadStream(this.filePath,{
380
+ start:this.pos,
381
+ end:st.size-1,
382
+ encoding:'utf8'
383
+ });
384
+
385
+ for await(const chunk of stream){
386
+ this.buf+=chunk;
387
+ let idx;
388
+ while((idx=this.buf.indexOf('\n'))>=0){
389
+ const line=this.buf.slice(0,idx);
390
+ this.buf=this.buf.slice(idx+1);
391
+ if(line.trim()) this.onLine(line);
392
+ }
393
+ }
394
+ this.pos=st.size;
395
+ }catch{}
396
+ }
397
+ close(){
398
+ try{ this.watch?.close(); }catch{}
399
+ }
400
+ }
401
+
402
+ const tail=new FileTail(LOG_PATH,line=>{
403
+ try{
404
+ if(!BAN_RE.test(line)) return;
405
+ const ip=extractIpFromLine(line);
406
+ if(!ip) return;
407
+ q.push(ip);
408
+ }catch(e){
409
+ log('onLine handler error',e.message||e);
410
+ }
411
+ });
412
+
413
+ function shutdown(){
414
+ log('Shutting down Fail2Scan...');
415
+ tail.close();
416
+ const start=Date.now();
417
+ const wait=()=>{
418
+ if(q.running===0||Date.now()-start>10000) process.exit(0);
419
+ setTimeout(wait,500);
420
+ };
421
+ wait();
196
422
  }
197
- 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);}});
198
423
 
199
- 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();}
200
424
  process.on('SIGINT',shutdown);
201
425
  process.on('SIGTERM',shutdown);
202
- //
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roflsec/fail2scan",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
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"
@@ -21,7 +21,6 @@
21
21
  "node": ">=18"
22
22
  },
23
23
  "dependencies": {
24
- "dotenv": "^17.2.3",
25
- "ip-geolocation-api-javascript-sdk": "^2.0.1"
24
+ "dotenv": "^17.2.3"
26
25
  }
27
26
  }