@roflsec/fail2scan 0.0.14 → 0.0.18

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 +84 -115
  2. package/package.json +1 -1
package/bin/daemon.js CHANGED
@@ -6,12 +6,11 @@ const { execFile, spawn } = require('child_process');
6
6
  const { promisify } = require('util');
7
7
  const execFileP = promisify(execFile);
8
8
 
9
- // === Géolocalisation ===
10
9
  let getGeo = async (ip) => null;
11
10
  if (process.env.IPGEO_API_KEY) {
12
- const { default: IPGeolocationAPI, GeolocationParams } = require("ip-geolocation-api-javascript-sdk");
11
+ const IPGeolocationAPI = require('ip-geolocation-api-javascript-sdk');
12
+ const { GeolocationParams } = require('ip-geolocation-api-javascript-sdk');
13
13
  const ipGeo = new IPGeolocationAPI(process.env.IPGEO_API_KEY, true);
14
-
15
14
  getGeo = (ip) => {
16
15
  return new Promise((resolve) => {
17
16
  const params = new GeolocationParams();
@@ -22,11 +21,9 @@ if (process.env.IPGEO_API_KEY) {
22
21
  };
23
22
  }
24
23
 
25
- // === Arguments CLI ===
26
24
  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')) {
29
- console.log(`Fail2Scan optimized daemon
25
+ 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
30
27
  --log PATH (default /var/log/fail2ban.log)
31
28
  --out PATH (default /var/log/fail2scan)
32
29
  --concurrency N (default 1)
@@ -34,41 +31,34 @@ if (argv.includes('--help') || argv.includes('-h')) {
34
31
  --nmap-args "args" (default "-sS -Pn -p- -T4 -sV")
35
32
  --scan-ip IP (do one scan and exit)
36
33
  --quiet
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);
34
+ `); process.exit(0); }
35
+
36
+ const LOG_PATH = getArg('--log','/var/log/fail2ban.log');
37
+ const OUT_ROOT = getArg('--out','/var/log/fail2scan');
38
+ const USER_CONCURRENCY = parseInt(getArg('--concurrency','0'),10)||0;
39
+ const CORE_OVERRIDE = parseInt(getArg('--cores','0'),10)||0;
40
+ const NMAP_ARGS_STR = getArg('--nmap-args','-sS -Pn -p- -T4 -sV');
41
+ const SINGLE_IP = getArg('--scan-ip',null);
46
42
  const QUIET = argv.includes('--quiet');
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');
43
+ const RESCAN_TTL_SEC = 60*60;
44
+ const STATE_FILE = path.join(os.homedir(),'.fail2scan_state.json');
45
+ const LOG_FILE = path.join(os.homedir(),'.fail2scan.log');
50
46
 
51
- // === Logging ===
52
- const log = (...a) => { if (!QUIET) console.log(new Date().toISOString(), ...a); try { fs.appendFileSync(LOG_FILE, new Date().toISOString() + ' ' + a.join(' ') + '\n'); } catch { } };
47
+ const log=(...a)=>{if(!QUIET)console.log(new Date().toISOString(),...a); try{fs.appendFileSync(LOG_FILE,new Date().toISOString()+' '+a.join(' ')+'\n');}catch{}};
53
48
 
54
- // === Utilitaires ===
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; } } }
49
+ 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;}}}
59
53
 
60
- // === Vérification prérequis ===
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); });
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);});
62
55
 
63
- // === Etat ===
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();
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
+ const STATE=loadState();
67
59
 
68
- // === Extraction IP ===
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; }
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;}
70
61
 
71
- // === Nmap spawn ===
72
62
  function spawnOneNmap(args, outFile) {
73
63
  return new Promise((resolve, reject) => {
74
64
  const proc = spawn('nmap', args, { stdio: ['ignore', 'pipe', 'pipe'] });
@@ -78,7 +68,7 @@ function spawnOneNmap(args, outFile) {
78
68
  proc.stderr.on('data', c => { stderr += c.toString(); });
79
69
  proc.on('close', code => {
80
70
  if (stderr) {
81
- try { fs.appendFileSync(outFile, '\n\nSTDERR:\n' + stderr); } catch (e) { }
71
+ try { fs.appendFileSync(outFile, '\n\nSTDERR:\n' + stderr); } catch (e) {}
82
72
  }
83
73
  resolve({ code, ok: code === 0 });
84
74
  });
@@ -102,7 +92,7 @@ async function spawnNmapParallel(ip, outDir, requestedArgs, parts) {
102
92
  const start = 1 + i * portsPer;
103
93
  const end = (i === numParts - 1) ? 65535 : ((i + 1) * portsPer);
104
94
  const subdir = path.join(outDir, `part-${i}`);
105
- try { fs.mkdirSync(subdir, { recursive: true, mode: 0o750 }); } catch (e) { }
95
+ try { fs.mkdirSync(subdir, { recursive: true, mode: 0o750 }); } catch (e) {}
106
96
  const outNmap = path.join(subdir, 'nmap.txt');
107
97
  const portArg = `-p${start}-${end}`;
108
98
  const args = requestedArgs.map(a => a === '-p-' ? portArg : a).concat([ip]);
@@ -115,10 +105,10 @@ async function spawnNmapParallel(ip, outDir, requestedArgs, parts) {
115
105
  for (let i = 0; i < numParts; i++) {
116
106
  const fn = path.join(outDir, `part-${i}`, 'nmap.txt');
117
107
  try {
118
- if (fs.existsSync(fn)) partsContent.push(fs.readFileSync(fn, 'utf8'));
119
- } catch (e) { }
108
+ if (fs.existsSync(fn)) partsContent.push(fs.readFileSync(fn,'utf8'));
109
+ } catch (e) {}
120
110
  }
121
- try { fs.writeFileSync(path.join(outDir, 'nmap.txt'), partsContent.join('\n\n--- PART ---\n\n')); } catch (e) { }
111
+ try { fs.writeFileSync(path.join(outDir, 'nmap.txt'), partsContent.join('\n\n--- PART ---\n\n')); } catch (e) {}
122
112
  }
123
113
 
124
114
  async function runNmap(ip, outDir, requestedArgs) {
@@ -129,105 +119,84 @@ async function runNmap(ip, outDir, requestedArgs) {
129
119
  await spawnNmapParallel(ip, outDir, args, parts);
130
120
  }
131
121
 
132
- // === Scan d’une IP ===
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: {} };
122
+ async function performScan(ip){
123
+ const now=new Date(),dateDir=now.toISOString().slice(0,10),safeIp=sanitizeFilename(ip);
124
+ let outDir=path.join(OUT_ROOT,dateDir,`${safeIp}_${now.toISOString().replace(/[:.]/g,'-')}`);
125
+ outDir=path.join(safeMkdirSyncWithFallback(path.dirname(outDir)),path.basename(outDir));
126
+ try { fs.mkdirSync(outDir,{recursive:true,mode:0o750}); } catch (e) {}
127
+ const summary={ip,ts:now.toISOString(),cmds:{}};
139
128
 
140
- const requested = NMAP_ARGS_STR.trim().split(/\s+/).filter(Boolean);
129
+ const requested=NMAP_ARGS_STR.trim().split(/\s+/).filter(Boolean);
141
130
 
142
- try {
143
- log('Running nmap on', ip, 'args:', requested.join(' '));
131
+ try{
132
+ log('Running nmap on',ip,'args:',requested.join(' '));
144
133
  await runNmap(ip, outDir, requested);
145
134
  summary.cmds.nmap = { ok: true, args: requested.join(' '), path: 'nmap.txt' };
146
- } catch (e) {
147
- log('nmap failed for', ip, e && e.message ? e.message : e);
135
+ } catch(e){
136
+ log('nmap failed for',ip, e && e.message ? e.message : e);
148
137
  summary.cmds.nmap = { ok: false, err: e && e.message ? e.message : String(e) };
149
138
  }
150
139
 
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) }; }
156
-
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) }; }
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) }; }
162
142
 
143
+ // Geolocation
163
144
  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 = []; }
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) }; }
167
149
 
168
- // === Géolocalisation ===
169
- if (getGeo) {
170
- try {
171
- const geo = await getGeo(ip);
172
- fs.writeFileSync(path.join(outDir, 'geo.json'), JSON.stringify(geo, null, 2));
173
- summary.geo = geo;
174
- } catch (e) {
175
- log('Geo lookup failed for', ip, e.message || e);
176
- summary.geo = null;
177
- }
178
- }
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 = []; }
179
151
 
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);
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
+ log('Scan written for',ip,'->',outDir);
183
155
  }
184
156
 
185
- // === Queue ===
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();
157
+ class ScanQueue{
158
+ constructor(concurrency=1){this.concurrency=concurrency;this.running=0;this.q=[];this.set=STATE.seen;this.tmpCache=new Set();}
159
+ 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();
194
165
  }
195
- _next() {
196
- if (this.running >= this.concurrency) return;
197
- const ip = this.q.shift(); if (!ip) return;
166
+ _next(){
167
+ if(this.running>=this.concurrency)return;
168
+ const ip=this.q.shift();if(!ip)return;
198
169
  this.running++;
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;
170
+ (async()=>{
171
+ try{log('Scanning',ip);await performScan(ip);log('Done',ip);}
172
+ catch(e){log('Error scanning',ip,e.message||e);}
173
+ finally{
174
+ STATE.retryAfter[ip]=Math.floor(Date.now()/1000)+RESCAN_TTL_SEC;
204
175
  saveState(STATE);
205
- this.set.delete(ip); this.tmpCache.delete(ip);
206
- this.running--; setImmediate(() => this._next());
176
+ this.set.delete(ip);this.tmpCache.delete(ip);
177
+ this.running--;setImmediate(()=>this._next());
207
178
  }
208
179
  })();
209
- setImmediate(() => this._next());
180
+ setImmediate(()=>this._next());
210
181
  }
211
182
  }
212
183
 
213
- // === Single IP mode ===
214
- if (SINGLE_IP) { (async () => { const q = new ScanQueue(CORE_OVERRIDE || USER_CONCURRENCY || 1); q.push(SINGLE_IP); })(); return; }
215
-
216
- // === Lancement daemon ===
217
- const concurrency = CORE_OVERRIDE || USER_CONCURRENCY || os.cpus().length || 1;
184
+ if(SINGLE_IP){(async()=>{const q=new ScanQueue(CORE_OVERRIDE||USER_CONCURRENCY||1);q.push(SINGLE_IP);})();return;}
185
+ const concurrency = CORE_OVERRIDE||USER_CONCURRENCY||os.cpus().length||1;
218
186
  const q = new ScanQueue(concurrency);
219
187
  log(`Fail2Scan started. Watching ${LOG_PATH} -> output ${OUT_ROOT}, concurrency ${concurrency}`);
220
188
 
221
189
  const BAN_RE = /\bBan\b/i;
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 { } }
190
+ 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{}}
228
196
  }
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); } });
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);}});
230
198
 
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);
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
+ process.on('SIGINT',shutdown);
201
+ process.on('SIGTERM',shutdown);
202
+ //
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roflsec/fail2scan",
3
- "version": "0.0.14",
3
+ "version": "0.0.18",
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"