@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.
- package/bin/daemon.js +96 -94
- 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')){
|
|
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
|
-
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
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()
|
|
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
|
|
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
|
|
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
|
|
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