@roflsec/fail2scan 0.0.11 → 0.0.13
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 +60 -55
- package/package.json +1 -1
package/bin/daemon.js
CHANGED
|
@@ -1,24 +1,28 @@
|
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
// === Géolocalisation ===
|
|
10
|
+
let getGeo = async (ip) => null;
|
|
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");
|
|
14
|
+
const ipGeo = new IPGeolocationAPI(process.env.IPGEO_API_KEY, true);
|
|
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
|
+
}
|
|
20
24
|
|
|
21
|
-
//
|
|
25
|
+
// === Arguments CLI ===
|
|
22
26
|
const argv = process.argv.slice(2);
|
|
23
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; };
|
|
24
28
|
if(argv.includes('--help')||argv.includes('-h')){console.log(`Fail2Scan optimized daemon
|
|
@@ -42,32 +46,27 @@ const RESCAN_TTL_SEC = 60*60;
|
|
|
42
46
|
const STATE_FILE = path.join(os.homedir(),'.fail2scan_state.json');
|
|
43
47
|
const LOG_FILE = path.join(os.homedir(),'.fail2scan.log');
|
|
44
48
|
|
|
49
|
+
// === Logging ===
|
|
45
50
|
const log=(...a)=>{if(!QUIET)console.log(new Date().toISOString(),...a); try{fs.appendFileSync(LOG_FILE,new Date().toISOString()+' '+a.join(' ')+'\n');}catch{}};
|
|
46
51
|
|
|
47
|
-
//
|
|
52
|
+
// === Utilitaires ===
|
|
48
53
|
const sanitizeFilename=s=>String(s).replace(/[:\/\\<>?"|* ]+/g,'_');
|
|
49
54
|
async function which(bin){try{await execFileP('which',[bin]);return true;}catch{return false;}}
|
|
50
55
|
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)+''};}}
|
|
51
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;}}}
|
|
52
57
|
|
|
53
|
-
//
|
|
58
|
+
// === Vérification prérequis ===
|
|
54
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);});
|
|
55
60
|
|
|
56
|
-
//
|
|
61
|
+
// === Etat ===
|
|
57
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:{}};}
|
|
58
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{}}
|
|
59
64
|
const STATE=loadState();
|
|
60
65
|
|
|
61
|
-
//
|
|
62
|
-
function extractIpFromLine(line){
|
|
63
|
-
const v4 = line.match(/\b(?:\d{1,3}\.){3}\d{1,3}\b/);
|
|
64
|
-
if(v4&&v4[0]) return v4[0];
|
|
65
|
-
const v6 = line.match(/\b([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}\b/);
|
|
66
|
-
if(v6&&v6[0]) return v6[0];
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
66
|
+
// === 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
68
|
|
|
70
|
-
//
|
|
69
|
+
// === Nmap spawn ===
|
|
71
70
|
function spawnOneNmap(args, outFile) {
|
|
72
71
|
return new Promise((resolve, reject) => {
|
|
73
72
|
const proc = spawn('nmap', args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
@@ -114,7 +113,7 @@ async function spawnNmapParallel(ip, outDir, requestedArgs, parts) {
|
|
|
114
113
|
for (let i = 0; i < numParts; i++) {
|
|
115
114
|
const fn = path.join(outDir, `part-${i}`, 'nmap.txt');
|
|
116
115
|
try {
|
|
117
|
-
if (fs.existsSync(fn)) partsContent.push(fs.readFileSync(fn,
|
|
116
|
+
if (fs.existsSync(fn)) partsContent.push(fs.readFileSync(fn,'utf8'));
|
|
118
117
|
} catch (e) {}
|
|
119
118
|
}
|
|
120
119
|
try { fs.writeFileSync(path.join(outDir, 'nmap.txt'), partsContent.join('\n\n--- PART ---\n\n')); } catch (e) {}
|
|
@@ -128,7 +127,7 @@ async function runNmap(ip, outDir, requestedArgs) {
|
|
|
128
127
|
await spawnNmapParallel(ip, outDir, args, parts);
|
|
129
128
|
}
|
|
130
129
|
|
|
131
|
-
//
|
|
130
|
+
// === Scan d’une IP ===
|
|
132
131
|
async function performScan(ip){
|
|
133
132
|
const now=new Date(),dateDir=now.toISOString().slice(0,10),safeIp=sanitizeFilename(ip);
|
|
134
133
|
let outDir=path.join(OUT_ROOT,dateDir,`${safeIp}_${now.toISOString().replace(/[:.]/g,'-')}`);
|
|
@@ -147,24 +146,41 @@ async function performScan(ip){
|
|
|
147
146
|
summary.cmds.nmap = { ok: false, err: e && e.message ? e.message : String(e) };
|
|
148
147
|
}
|
|
149
148
|
|
|
150
|
-
try{
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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) }; }
|
|
154
|
+
|
|
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) }; }
|
|
160
|
+
|
|
161
|
+
try{
|
|
162
|
+
const nmapTxt = fs.readFileSync(path.join(outDir,'nmap.txt'),'utf8');
|
|
163
|
+
summary.open_ports = nmapTxt.split(/\r?\n/).filter(l=>/^\d+\/tcp\s+open/.test(l)).map(l=>l.trim());
|
|
164
|
+
} catch(e){ summary.open_ports = []; }
|
|
165
|
+
|
|
166
|
+
// === Géolocalisation ===
|
|
167
|
+
if(getGeo){
|
|
168
|
+
try{
|
|
169
|
+
const geo = await getGeo(ip);
|
|
170
|
+
fs.writeFileSync(path.join(outDir,'geo.json'), JSON.stringify(geo,null,2));
|
|
171
|
+
summary.geo = geo;
|
|
172
|
+
}catch(e){
|
|
173
|
+
log('Geo lookup failed for',ip,e.message||e);
|
|
174
|
+
summary.geo = null;
|
|
175
|
+
}
|
|
161
176
|
}
|
|
162
177
|
|
|
178
|
+
try{ fs.writeFileSync(path.join(outDir,'summary.json'),JSON.stringify(summary,null,2)); } catch (e) {}
|
|
163
179
|
try{ fs.chmodSync(outDir,0o750); } catch (e) {}
|
|
164
180
|
log('Scan written for',ip,'->',outDir);
|
|
165
181
|
}
|
|
166
182
|
|
|
167
|
-
//
|
|
183
|
+
// === Queue ===
|
|
168
184
|
class ScanQueue{
|
|
169
185
|
constructor(concurrency=1){this.concurrency=concurrency;this.running=0;this.q=[];this.set=STATE.seen;this.tmpCache=new Set();}
|
|
170
186
|
push(ip){
|
|
@@ -192,15 +208,15 @@ class ScanQueue{
|
|
|
192
208
|
}
|
|
193
209
|
}
|
|
194
210
|
|
|
195
|
-
//
|
|
211
|
+
// === Single IP mode ===
|
|
196
212
|
if(SINGLE_IP){(async()=>{const q=new ScanQueue(CORE_OVERRIDE||USER_CONCURRENCY||1);q.push(SINGLE_IP);})();return;}
|
|
213
|
+
|
|
214
|
+
// === Lancement daemon ===
|
|
197
215
|
const concurrency = CORE_OVERRIDE||USER_CONCURRENCY||os.cpus().length||1;
|
|
198
216
|
const q = new ScanQueue(concurrency);
|
|
199
217
|
log(`Fail2Scan started. Watching ${LOG_PATH} -> output ${OUT_ROOT}, concurrency ${concurrency}`);
|
|
200
218
|
|
|
201
|
-
|
|
202
|
-
const BAN_RE = /Ban\s+(\d{1,3}(?:\.\d{1,3}){3})/;
|
|
203
|
-
|
|
219
|
+
const BAN_RE = /\bBan\b/i;
|
|
204
220
|
class FileTail{
|
|
205
221
|
constructor(filePath,onLine){this.filePath=filePath;this.onLine=onLine;this.pos=0;this.inode=null;this.buf='';this.watch=null;this.start();}
|
|
206
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(()=>{});}
|
|
@@ -208,18 +224,7 @@ class FileTail{
|
|
|
208
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{}}
|
|
209
225
|
close(){try{this.watch?.close();}catch{}}
|
|
210
226
|
}
|
|
211
|
-
|
|
212
|
-
const tail=new FileTail(LOG_PATH,line=>{
|
|
213
|
-
try{
|
|
214
|
-
const m=line.match(BAN_RE);
|
|
215
|
-
if(!m) return;
|
|
216
|
-
const ip = m[1];
|
|
217
|
-
if(!ip) return;
|
|
218
|
-
q.push(ip);
|
|
219
|
-
}catch(e){
|
|
220
|
-
log('onLine handler error',e.message||e);
|
|
221
|
-
}
|
|
222
|
-
});
|
|
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);}});
|
|
223
228
|
|
|
224
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();}
|
|
225
230
|
process.on('SIGINT',shutdown);
|
package/package.json
CHANGED