@roflsec/fail2scan 0.0.17 → 0.0.19

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 +285 -61
  2. package/package.json +2 -3
package/bin/daemon.js CHANGED
@@ -1,28 +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
8
 
9
- let getGeo = async (ip) => null;
10
- if (process.env.IPGEO_API_KEY) {
11
- const { default: IPGeolocationAPI, GeolocationParams } = require("ip-geolocation-api-javascript-sdk");
12
- const ipGeo = new IPGeolocationAPI(process.env.IPGEO_API_KEY, true);
13
- getGeo = (ip) => {
14
- return new Promise((resolve) => {
15
- const params = new GeolocationParams();
16
- params.setIPAddress(ip);
17
- params.setFields("geo,time_zone,currency,asn,security");
18
- ipGeo.getGeolocation((res) => resolve(res), params);
19
- });
20
- };
21
- }
22
-
9
+ // -------------------- CLI / CONFIG --------------------
23
10
  const argv = process.argv.slice(2);
24
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; };
25
- 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
26
15
  --log PATH (default /var/log/fail2ban.log)
27
16
  --out PATH (default /var/log/fail2scan)
28
17
  --concurrency N (default 1)
@@ -30,7 +19,9 @@ if(argv.includes('--help')||argv.includes('-h')){console.log(`Fail2Scan optimize
30
19
  --nmap-args "args" (default "-sS -Pn -p- -T4 -sV")
31
20
  --scan-ip IP (do one scan and exit)
32
21
  --quiet
33
- `); process.exit(0); }
22
+ `);
23
+ process.exit(0);
24
+ }
34
25
 
35
26
  const LOG_PATH = getArg('--log','/var/log/fail2ban.log');
36
27
  const OUT_ROOT = getArg('--out','/var/log/fail2scan');
@@ -43,21 +34,107 @@ const RESCAN_TTL_SEC = 60*60;
43
34
  const STATE_FILE = path.join(os.homedir(),'.fail2scan_state.json');
44
35
  const LOG_FILE = path.join(os.homedir(),'.fail2scan.log');
45
36
 
46
- 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
+ };
47
41
 
42
+ // -------------------- utilities --------------------
48
43
  const sanitizeFilename=s=>String(s).replace(/[:\/\\<>?"|* ]+/g,'_');
49
- async function which(bin){try{await execFileP('which',[bin]);return true;}catch{return false;}}
50
- 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)+''};}}
51
- 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;}}}
52
44
 
53
- (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
+ }
54
105
 
55
- 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:{}};}
56
- 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{}}
57
106
  const STATE=loadState();
58
107
 
59
- 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" });
60
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 --------------------
61
138
  function spawnOneNmap(args, outFile) {
62
139
  return new Promise((resolve, reject) => {
63
140
  const proc = spawn('nmap', args, { stdio: ['ignore', 'pipe', 'pipe'] });
@@ -84,8 +161,8 @@ async function spawnNmapParallel(ip, outDir, requestedArgs, parts) {
84
161
 
85
162
  const cpuCount = os.cpus() ? os.cpus().length : 1;
86
163
  const numParts = Math.max(1, parts || CORE_OVERRIDE || cpuCount);
87
-
88
164
  const portsPer = Math.floor(65535 / numParts);
165
+
89
166
  const jobs = [];
90
167
  for (let i = 0; i < numParts; i++) {
91
168
  const start = 1 + i * portsPer;
@@ -97,14 +174,13 @@ async function spawnNmapParallel(ip, outDir, requestedArgs, parts) {
97
174
  const args = requestedArgs.map(a => a === '-p-' ? portArg : a).concat([ip]);
98
175
  jobs.push(spawnOneNmap(args, outNmap));
99
176
  }
100
-
101
177
  await Promise.all(jobs);
102
178
 
103
179
  const partsContent = [];
104
180
  for (let i = 0; i < numParts; i++) {
105
181
  const fn = path.join(outDir, `part-${i}`, 'nmap.txt');
106
182
  try {
107
- if (fs.existsSync(fn)) partsContent.push(fs.readFileSync(fn,'utf8'));
183
+ if (fs.existsSync(fn)) partsContent.push(fs.readFileSync(fn, 'utf8'));
108
184
  } catch (e) {}
109
185
  }
110
186
  try { fs.writeFileSync(path.join(outDir, 'nmap.txt'), partsContent.join('\n\n--- PART ---\n\n')); } catch (e) {}
@@ -118,17 +194,23 @@ async function runNmap(ip, outDir, requestedArgs) {
118
194
  await spawnNmapParallel(ip, outDir, args, parts);
119
195
  }
120
196
 
197
+ // -------------------- scan --------------------
121
198
  async function performScan(ip){
122
- 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
+
123
203
  let outDir=path.join(OUT_ROOT,dateDir,`${safeIp}_${now.toISOString().replace(/[:.]/g,'-')}`);
124
204
  outDir=path.join(safeMkdirSyncWithFallback(path.dirname(outDir)),path.basename(outDir));
125
205
  try { fs.mkdirSync(outDir,{recursive:true,mode:0o750}); } catch (e) {}
206
+
126
207
  const summary={ip,ts:now.toISOString(),cmds:{}};
127
208
 
128
209
  const requested=NMAP_ARGS_STR.trim().split(/\s+/).filter(Boolean);
129
210
 
211
+ log('Running nmap on',ip,'args:',requested.join(' '));
212
+
130
213
  try{
131
- log('Running nmap on',ip,'args:',requested.join(' '));
132
214
  await runNmap(ip, outDir, requested);
133
215
  summary.cmds.nmap = { ok: true, args: requested.join(' '), path: 'nmap.txt' };
134
216
  } catch(e){
@@ -136,66 +218,208 @@ async function performScan(ip){
136
218
  summary.cmds.nmap = { ok: false, err: e && e.message ? e.message : String(e) };
137
219
  }
138
220
 
139
- 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) }; }
140
- 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
+ }
141
240
 
142
- // Geolocation
241
+ // ---- GEOLOCATION ----
143
242
  try {
144
- const geo = await getGeo(ip);
145
- if (geo) fs.writeFileSync(path.join(outDir,'geo.json'),JSON.stringify(geo,null,2));
146
- summary.cmds.geo = { ok: !!geo, path: 'geo.json' };
147
- } 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
+ }
148
260
 
149
- 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{}
150
263
 
151
- try{ fs.writeFileSync(path.join(outDir,'summary.json'),JSON.stringify(summary,null,2)); } catch (e) {}
152
- try{ fs.chmodSync(outDir,0o750); } catch (e) {}
153
264
  log('Scan written for',ip,'->',outDir);
154
265
  }
155
266
 
267
+ // -------------------- queue optimized --------------------
156
268
  class ScanQueue{
157
- 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
+ }
158
276
  push(ip){
159
- const now=Math.floor(Date.now()/1000),next=STATE.retryAfter[ip]||0;
160
- if((this.set.has(ip)&&next>now)||this.tmpCache.has(ip)){log('IP already queued or running (TTL/cache):',ip);return;}
161
- if(this.set.has(ip)&&next<=now)log('Re-queueing after TTL:',ip),this.set.delete(ip);
162
- this.set.add(ip);STATE.seen=this.set;saveState(STATE);
163
- 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();
164
293
  }
165
294
  _next(){
166
295
  if(this.running>=this.concurrency)return;
167
- const ip=this.q.shift();if(!ip)return;
296
+ const ip=this.q.shift(); if(!ip) return;
168
297
  this.running++;
169
298
  (async()=>{
170
- try{log('Scanning',ip);await performScan(ip);log('Done',ip);}
171
- catch(e){log('Error scanning',ip,e.message||e);}
172
- 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{
173
306
  STATE.retryAfter[ip]=Math.floor(Date.now()/1000)+RESCAN_TTL_SEC;
174
307
  saveState(STATE);
175
- this.set.delete(ip);this.tmpCache.delete(ip);
176
- this.running--;setImmediate(()=>this._next());
308
+ this.set.delete(ip);
309
+ this.tmpCache.delete(ip);
310
+ this.running--;
311
+ setImmediate(()=>this._next());
177
312
  }
178
313
  })();
179
314
  setImmediate(()=>this._next());
180
315
  }
181
316
  }
182
317
 
183
- 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
+
184
327
  const concurrency = CORE_OVERRIDE||USER_CONCURRENCY||os.cpus().length||1;
185
328
  const q = new ScanQueue(concurrency);
186
329
  log(`Fail2Scan started. Watching ${LOG_PATH} -> output ${OUT_ROOT}, concurrency ${concurrency}`);
187
330
 
188
331
  const BAN_RE = /\bBan\b/i;
332
+
189
333
  class FileTail{
190
- constructor(filePath,onLine){this.filePath=filePath;this.onLine=onLine;this.pos=0;this.inode=null;this.buf='';this.watch=null;this.start();}
191
- 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(()=>{});}
192
- _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);}}
193
- 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{}}
194
- 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();
195
422
  }
196
- 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);}});
197
423
 
198
- 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();}
199
424
  process.on('SIGINT',shutdown);
200
425
  process.on('SIGTERM',shutdown);
201
- //
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roflsec/fail2scan",
3
- "version": "0.0.17",
3
+ "version": "0.0.19",
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
  }