@roflsec/fail2scan 0.0.12 → 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 +135 -124
- package/package.json +1 -1
package/bin/daemon.js
CHANGED
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
|
+
require('dotenv').config();
|
|
3
4
|
const fs = require('fs'), path = require('path'), os = require('os');
|
|
4
5
|
const { execFile, spawn } = require('child_process');
|
|
5
6
|
const { promisify } = require('util');
|
|
6
7
|
const execFileP = promisify(execFile);
|
|
7
|
-
require('dotenv').config({ quiet:true });
|
|
8
|
-
const IPGeolocationAPI = require("ip-geolocation-api-javascript-sdk");
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
// === Géolocalisation ===
|
|
10
|
+
let getGeo = async (ip) => null;
|
|
11
|
+
if (process.env.IPGEO_API_KEY) {
|
|
12
|
+
const { default: IPGeolocationAPI, GeolocationParams } = require("ip-geolocation-api-javascript-sdk");
|
|
13
|
+
const ipGeo = new IPGeolocationAPI(process.env.IPGEO_API_KEY, true);
|
|
14
|
+
|
|
15
|
+
getGeo = (ip) => {
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
const params = new GeolocationParams();
|
|
18
|
+
params.setIPAddress(ip);
|
|
19
|
+
params.setFields("geo,time_zone,currency,asn,security");
|
|
20
|
+
ipGeo.getGeolocation((res) => resolve(res), params);
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
}
|
|
16
24
|
|
|
17
|
-
//
|
|
25
|
+
// === Arguments CLI ===
|
|
18
26
|
const argv = process.argv.slice(2);
|
|
19
|
-
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; };
|
|
20
|
-
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
|
|
21
30
|
--log PATH (default /var/log/fail2ban.log)
|
|
22
31
|
--out PATH (default /var/log/fail2scan)
|
|
23
32
|
--concurrency N (default 1)
|
|
@@ -25,45 +34,41 @@ if(argv.includes('--help')||argv.includes('-h')){console.log(`Fail2Scan optimize
|
|
|
25
34
|
--nmap-args "args" (default "-sS -Pn -p- -T4 -sV")
|
|
26
35
|
--scan-ip IP (do one scan and exit)
|
|
27
36
|
--quiet
|
|
28
|
-
`); process.exit(0);
|
|
29
|
-
|
|
30
|
-
const LOG_PATH = getArg('--log','/var/log/fail2ban.log');
|
|
31
|
-
const OUT_ROOT = getArg('--out','/var/log/fail2scan');
|
|
32
|
-
const USER_CONCURRENCY = parseInt(getArg('--concurrency','0'),10)||0;
|
|
33
|
-
const CORE_OVERRIDE = parseInt(getArg('--cores','0'),10)||0;
|
|
34
|
-
const NMAP_ARGS_STR = getArg('--nmap-args','-sS -Pn -p- -T4 -sV');
|
|
35
|
-
const SINGLE_IP = getArg('--scan-ip',null);
|
|
36
|
-
const QUIET = argv.includes('--quiet');
|
|
37
|
-
const RESCAN_TTL_SEC = 60*60;
|
|
38
|
-
const STATE_FILE = path.join(os.homedir(),'.fail2scan_state.json');
|
|
39
|
-
const LOG_FILE = path.join(os.homedir(),'.fail2scan.log');
|
|
40
|
-
|
|
41
|
-
const log=(...a)=>{if(!QUIET)console.log(new Date().toISOString(),...a); try{fs.appendFileSync(LOG_FILE,new Date().toISOString()+' '+a.join(' ')+'\n');}catch{}};
|
|
42
|
-
|
|
43
|
-
// -------------------- utilities --------------------
|
|
44
|
-
const sanitizeFilename=s=>String(s).replace(/[:\/\\<>?"|* ]+/g,'_');
|
|
45
|
-
async function which(bin){try{await execFileP('which',[bin]);return true;}catch{return false;}}
|
|
46
|
-
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)+''};}}
|
|
47
|
-
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;}}}
|
|
48
|
-
|
|
49
|
-
// -------------------- prerequisites --------------------
|
|
50
|
-
(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);});
|
|
51
|
-
|
|
52
|
-
// -------------------- state --------------------
|
|
53
|
-
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:{}};}
|
|
54
|
-
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{}}
|
|
55
|
-
const STATE=loadState();
|
|
56
|
-
|
|
57
|
-
// -------------------- IP extraction --------------------
|
|
58
|
-
function extractIpFromLine(line){
|
|
59
|
-
const v4 = line.match(/\b(?:\d{1,3}\.){3}\d{1,3}\b/);
|
|
60
|
-
if(v4&&v4[0]) return v4[0];
|
|
61
|
-
const v6 = line.match(/\b([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}\b/);
|
|
62
|
-
if(v6&&v6[0]) return v6[0];
|
|
63
|
-
return null;
|
|
37
|
+
`); process.exit(0);
|
|
64
38
|
}
|
|
65
39
|
|
|
66
|
-
|
|
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);
|
|
46
|
+
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');
|
|
50
|
+
|
|
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 { } };
|
|
53
|
+
|
|
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; } } }
|
|
59
|
+
|
|
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); });
|
|
62
|
+
|
|
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();
|
|
67
|
+
|
|
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; }
|
|
70
|
+
|
|
71
|
+
// === Nmap spawn ===
|
|
67
72
|
function spawnOneNmap(args, outFile) {
|
|
68
73
|
return new Promise((resolve, reject) => {
|
|
69
74
|
const proc = spawn('nmap', args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
@@ -73,7 +78,7 @@ function spawnOneNmap(args, outFile) {
|
|
|
73
78
|
proc.stderr.on('data', c => { stderr += c.toString(); });
|
|
74
79
|
proc.on('close', code => {
|
|
75
80
|
if (stderr) {
|
|
76
|
-
try { fs.appendFileSync(outFile, '\n\nSTDERR:\n' + stderr); } catch (e) {}
|
|
81
|
+
try { fs.appendFileSync(outFile, '\n\nSTDERR:\n' + stderr); } catch (e) { }
|
|
77
82
|
}
|
|
78
83
|
resolve({ code, ok: code === 0 });
|
|
79
84
|
});
|
|
@@ -97,7 +102,7 @@ async function spawnNmapParallel(ip, outDir, requestedArgs, parts) {
|
|
|
97
102
|
const start = 1 + i * portsPer;
|
|
98
103
|
const end = (i === numParts - 1) ? 65535 : ((i + 1) * portsPer);
|
|
99
104
|
const subdir = path.join(outDir, `part-${i}`);
|
|
100
|
-
try { fs.mkdirSync(subdir, { recursive: true, mode: 0o750 }); } catch (e) {}
|
|
105
|
+
try { fs.mkdirSync(subdir, { recursive: true, mode: 0o750 }); } catch (e) { }
|
|
101
106
|
const outNmap = path.join(subdir, 'nmap.txt');
|
|
102
107
|
const portArg = `-p${start}-${end}`;
|
|
103
108
|
const args = requestedArgs.map(a => a === '-p-' ? portArg : a).concat([ip]);
|
|
@@ -111,9 +116,9 @@ async function spawnNmapParallel(ip, outDir, requestedArgs, parts) {
|
|
|
111
116
|
const fn = path.join(outDir, `part-${i}`, 'nmap.txt');
|
|
112
117
|
try {
|
|
113
118
|
if (fs.existsSync(fn)) partsContent.push(fs.readFileSync(fn, 'utf8'));
|
|
114
|
-
} catch (e) {}
|
|
119
|
+
} catch (e) { }
|
|
115
120
|
}
|
|
116
|
-
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) { }
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
async function runNmap(ip, outDir, requestedArgs) {
|
|
@@ -124,99 +129,105 @@ async function runNmap(ip, outDir, requestedArgs) {
|
|
|
124
129
|
await spawnNmapParallel(ip, outDir, args, parts);
|
|
125
130
|
}
|
|
126
131
|
|
|
127
|
-
//
|
|
128
|
-
async function performScan(ip){
|
|
129
|
-
const now=new Date(),dateDir=now.toISOString().slice(0,10),safeIp=sanitizeFilename(ip);
|
|
130
|
-
let outDir=path.join(OUT_ROOT,dateDir
|
|
131
|
-
outDir=path.join(safeMkdirSyncWithFallback(path.dirname(outDir)),path.basename(outDir));
|
|
132
|
-
try { fs.mkdirSync(outDir,{recursive:true,mode:0o750}); } catch (e) {}
|
|
133
|
-
const summary={ip,ts:now.toISOString(),cmds:{}};
|
|
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: {} };
|
|
134
139
|
|
|
135
|
-
const requested=NMAP_ARGS_STR.trim().split(/\s+/).filter(Boolean);
|
|
140
|
+
const requested = NMAP_ARGS_STR.trim().split(/\s+/).filter(Boolean);
|
|
136
141
|
|
|
137
|
-
try{
|
|
138
|
-
log('Running nmap on',ip,'args:',requested.join(' '));
|
|
142
|
+
try {
|
|
143
|
+
log('Running nmap on', ip, 'args:', requested.join(' '));
|
|
139
144
|
await runNmap(ip, outDir, requested);
|
|
140
145
|
summary.cmds.nmap = { ok: true, args: requested.join(' '), path: 'nmap.txt' };
|
|
141
|
-
} catch(e){
|
|
142
|
-
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);
|
|
143
148
|
summary.cmds.nmap = { ok: false, err: e && e.message ? e.message : String(e) };
|
|
144
149
|
}
|
|
145
150
|
|
|
146
|
-
try
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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) }; }
|
|
162
|
+
|
|
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 = []; }
|
|
167
|
+
|
|
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
|
+
}
|
|
157
178
|
}
|
|
158
179
|
|
|
159
|
-
try{ fs.
|
|
160
|
-
|
|
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);
|
|
161
183
|
}
|
|
162
184
|
|
|
163
|
-
//
|
|
164
|
-
class ScanQueue{
|
|
165
|
-
constructor(concurrency=1){this.concurrency=concurrency;this.running=0;this.q=[];this.set=STATE.seen;this.tmpCache=new Set();}
|
|
166
|
-
push(ip){
|
|
167
|
-
const now=Math.floor(Date.now()/1000),next=STATE.retryAfter[ip]||0;
|
|
168
|
-
if((this.set.has(ip)&&next>now)||this.tmpCache.has(ip)){log('IP already queued or running (TTL/cache):',ip);return;}
|
|
169
|
-
if(this.set.has(ip)&&next<=now)log('Re-queueing after TTL:',ip),this.set.delete(ip);
|
|
170
|
-
this.set.add(ip);STATE.seen=this.set;saveState(STATE);
|
|
171
|
-
this.q.push(ip);this.tmpCache.add(ip);this._next();
|
|
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();
|
|
172
194
|
}
|
|
173
|
-
_next(){
|
|
174
|
-
if(this.running>=this.concurrency)return;
|
|
175
|
-
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;
|
|
176
198
|
this.running++;
|
|
177
|
-
(async()=>{
|
|
178
|
-
try{log('Scanning',ip);await performScan(ip);log('Done',ip);}
|
|
179
|
-
catch(e){log('Error scanning',ip,e.message||e);}
|
|
180
|
-
finally{
|
|
181
|
-
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;
|
|
182
204
|
saveState(STATE);
|
|
183
|
-
this.set.delete(ip);this.tmpCache.delete(ip);
|
|
184
|
-
this.running--;setImmediate(()=>this._next());
|
|
205
|
+
this.set.delete(ip); this.tmpCache.delete(ip);
|
|
206
|
+
this.running--; setImmediate(() => this._next());
|
|
185
207
|
}
|
|
186
208
|
})();
|
|
187
|
-
setImmediate(()=>this._next());
|
|
209
|
+
setImmediate(() => this._next());
|
|
188
210
|
}
|
|
189
211
|
}
|
|
190
212
|
|
|
191
|
-
//
|
|
192
|
-
if(SINGLE_IP){(async()=>{const q=new ScanQueue(CORE_OVERRIDE||USER_CONCURRENCY||1);q.push(SINGLE_IP);})();return;}
|
|
193
|
-
|
|
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;
|
|
194
218
|
const q = new ScanQueue(concurrency);
|
|
195
219
|
log(`Fail2Scan started. Watching ${LOG_PATH} -> output ${OUT_ROOT}, concurrency ${concurrency}`);
|
|
196
220
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
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{}}
|
|
205
|
-
close(){try{this.watch?.close();}catch{}}
|
|
221
|
+
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 { } }
|
|
206
228
|
}
|
|
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); } });
|
|
207
230
|
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if(!m) return;
|
|
212
|
-
const ip = m[1];
|
|
213
|
-
if(!ip) return;
|
|
214
|
-
q.push(ip);
|
|
215
|
-
}catch(e){
|
|
216
|
-
log('onLine handler error',e.message||e);
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
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();}
|
|
221
|
-
process.on('SIGINT',shutdown);
|
|
222
|
-
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