@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.
- package/bin/daemon.js +286 -63
- 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
|
-
|
|
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
|
-
|
|
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
|
-
`);
|
|
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)=>{
|
|
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
|
-
|
|
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
|
-
|
|
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(),
|
|
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{
|
|
141
|
-
|
|
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
|
-
//
|
|
241
|
+
// ---- GEOLOCATION ----
|
|
144
242
|
try {
|
|
145
|
-
const geo = await
|
|
146
|
-
|
|
147
|
-
summary.cmds.geo = { ok:
|
|
148
|
-
} catch(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{
|
|
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){
|
|
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),
|
|
161
|
-
|
|
162
|
-
if(this.set.has(ip)&&next
|
|
163
|
-
|
|
164
|
-
|
|
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{
|
|
172
|
-
|
|
173
|
-
|
|
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);
|
|
177
|
-
this.
|
|
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
|
-
|
|
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){
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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.
|
|
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
|
}
|