@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.
Files changed (2) hide show
  1. package/bin/daemon.js +60 -55
  2. 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
- // ----------- Geo setup (fixed) -----------
11
- const geoApi = new IPGeolocationAPI(process.env.IPGEO_API_KEY, true);
12
- const getGeo = async (ip) => {
13
- return new Promise((resolve, reject) => {
14
- geoApi.getGeolocation(
15
- (res) => resolve(res),
16
- { ip, fields: "geo,time_zone,currency,asn,security" }
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
- // -------------------- CLI / CONFIG --------------------
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
- // -------------------- utilities --------------------
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
- // -------------------- prerequisites --------------------
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
- // -------------------- state --------------------
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
- // -------------------- IP extraction --------------------
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
- // -------------------- nmap helpers (parallel support) --------------------
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, 'utf8'));
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
- // -------------------- scan --------------------
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{ 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) }; }
151
- 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) }; }
152
-
153
- 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 = []; }
154
-
155
- try{ fs.writeFileSync(path.join(outDir,'summary.json'),JSON.stringify(summary,null,2)); } catch (e) {}
156
-
157
- if(process.env.IPGEO_API_KEY){
158
- getGeo(ip)
159
- .then(geo => { try{ fs.writeFileSync(path.join(outDir,'geo.json'), JSON.stringify(geo, null, 2)); }catch{} })
160
- .catch(()=>{});
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
- // -------------------- queue --------------------
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
- // -------------------- main --------------------
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
- // -------------------- FIX : regex BAN correct --------------------
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roflsec/fail2scan",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
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"