@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.
- package/bin/daemon.js +84 -115
- 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
|
|
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,
|
|
28
|
-
if
|
|
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
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
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
|
|
48
|
-
const STATE_FILE = path.join(os.homedir(),
|
|
49
|
-
const LOG_FILE = path.join(os.homedir(),
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
async function
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
function
|
|
65
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
129
|
+
const requested=NMAP_ARGS_STR.trim().split(/\s+/).filter(Boolean);
|
|
141
130
|
|
|
142
|
-
try
|
|
143
|
-
log('Running nmap on',
|
|
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
|
|
147
|
-
log('nmap failed for',
|
|
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
|
-
|
|
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
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|
|
181
|
-
try
|
|
182
|
-
log('Scan written for',
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if
|
|
191
|
-
|
|
192
|
-
this.
|
|
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
|
|
197
|
-
const ip
|
|
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
|
|
201
|
-
catch
|
|
202
|
-
finally
|
|
203
|
-
STATE.retryAfter[ip]
|
|
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);
|
|
206
|
-
this.running--;
|
|
176
|
+
this.set.delete(ip);this.tmpCache.delete(ip);
|
|
177
|
+
this.running--;setImmediate(()=>this._next());
|
|
207
178
|
}
|
|
208
179
|
})();
|
|
209
|
-
setImmediate(()
|
|
180
|
+
setImmediate(()=>this._next());
|
|
210
181
|
}
|
|
211
182
|
}
|
|
212
183
|
|
|
213
|
-
|
|
214
|
-
|
|
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,
|
|
224
|
-
start()
|
|
225
|
-
_watch()
|
|
226
|
-
async _readNew()
|
|
227
|
-
close()
|
|
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
|
|
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()
|
|
232
|
-
process.on('SIGINT',
|
|
233
|
-
process.on('SIGTERM',
|
|
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