@roflsec/fail2scan 0.0.12 → 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 -51
  2. package/package.json +1 -1
package/bin/daemon.js CHANGED
@@ -1,20 +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
- const geoApi = new IPGeolocationAPI(process.env.IPGEO_API_KEY, true);
11
- const getGeo = async (ip) => {
12
- return new Promise((resolve, reject) => {
13
- geoApi.getGeolocation(resolve, { ip, fields: "geo,time_zone,currency,asn,security" });
14
- });
15
- };
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
+ }
16
24
 
17
- // -------------------- CLI / CONFIG --------------------
25
+ // === Arguments CLI ===
18
26
  const argv = process.argv.slice(2);
19
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; };
20
28
  if(argv.includes('--help')||argv.includes('-h')){console.log(`Fail2Scan optimized daemon
@@ -38,32 +46,27 @@ const RESCAN_TTL_SEC = 60*60;
38
46
  const STATE_FILE = path.join(os.homedir(),'.fail2scan_state.json');
39
47
  const LOG_FILE = path.join(os.homedir(),'.fail2scan.log');
40
48
 
49
+ // === Logging ===
41
50
  const log=(...a)=>{if(!QUIET)console.log(new Date().toISOString(),...a); try{fs.appendFileSync(LOG_FILE,new Date().toISOString()+' '+a.join(' ')+'\n');}catch{}};
42
51
 
43
- // -------------------- utilities --------------------
52
+ // === Utilitaires ===
44
53
  const sanitizeFilename=s=>String(s).replace(/[:\/\\<>?"|* ]+/g,'_');
45
54
  async function which(bin){try{await execFileP('which',[bin]);return true;}catch{return false;}}
46
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)+''};}}
47
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;}}}
48
57
 
49
- // -------------------- prerequisites --------------------
58
+ // === Vérification prérequis ===
50
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);});
51
60
 
52
- // -------------------- state --------------------
61
+ // === Etat ===
53
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:{}};}
54
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{}}
55
64
  const STATE=loadState();
56
65
 
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;
64
- }
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;}
65
68
 
66
- // -------------------- nmap helpers --------------------
69
+ // === Nmap spawn ===
67
70
  function spawnOneNmap(args, outFile) {
68
71
  return new Promise((resolve, reject) => {
69
72
  const proc = spawn('nmap', args, { stdio: ['ignore', 'pipe', 'pipe'] });
@@ -110,7 +113,7 @@ async function spawnNmapParallel(ip, outDir, requestedArgs, parts) {
110
113
  for (let i = 0; i < numParts; i++) {
111
114
  const fn = path.join(outDir, `part-${i}`, 'nmap.txt');
112
115
  try {
113
- if (fs.existsSync(fn)) partsContent.push(fs.readFileSync(fn, 'utf8'));
116
+ if (fs.existsSync(fn)) partsContent.push(fs.readFileSync(fn,'utf8'));
114
117
  } catch (e) {}
115
118
  }
116
119
  try { fs.writeFileSync(path.join(outDir, 'nmap.txt'), partsContent.join('\n\n--- PART ---\n\n')); } catch (e) {}
@@ -124,7 +127,7 @@ async function runNmap(ip, outDir, requestedArgs) {
124
127
  await spawnNmapParallel(ip, outDir, args, parts);
125
128
  }
126
129
 
127
- // -------------------- scan --------------------
130
+ // === Scan d’une IP ===
128
131
  async function performScan(ip){
129
132
  const now=new Date(),dateDir=now.toISOString().slice(0,10),safeIp=sanitizeFilename(ip);
130
133
  let outDir=path.join(OUT_ROOT,dateDir,`${safeIp}_${now.toISOString().replace(/[:.]/g,'-')}`);
@@ -143,24 +146,41 @@ async function performScan(ip){
143
146
  summary.cmds.nmap = { ok: false, err: e && e.message ? e.message : String(e) };
144
147
  }
145
148
 
146
- 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) }; }
147
- 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) }; }
148
-
149
- 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 = []; }
150
-
151
- try{ fs.writeFileSync(path.join(outDir,'summary.json'),JSON.stringify(summary,null,2)); } catch (e) {}
152
-
153
- if(process.env.IPGEO_API_KEY){
154
- getGeo(ip)
155
- .then(geo => { try{ fs.writeFileSync(path.join(outDir,'geo.json'), JSON.stringify(geo, null, 2)); }catch{} })
156
- .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
+ }
157
176
  }
158
177
 
178
+ try{ fs.writeFileSync(path.join(outDir,'summary.json'),JSON.stringify(summary,null,2)); } catch (e) {}
159
179
  try{ fs.chmodSync(outDir,0o750); } catch (e) {}
160
180
  log('Scan written for',ip,'->',outDir);
161
181
  }
162
182
 
163
- // -------------------- queue --------------------
183
+ // === Queue ===
164
184
  class ScanQueue{
165
185
  constructor(concurrency=1){this.concurrency=concurrency;this.running=0;this.q=[];this.set=STATE.seen;this.tmpCache=new Set();}
166
186
  push(ip){
@@ -188,15 +208,15 @@ class ScanQueue{
188
208
  }
189
209
  }
190
210
 
191
- // -------------------- main --------------------
211
+ // === Single IP mode ===
192
212
  if(SINGLE_IP){(async()=>{const q=new ScanQueue(CORE_OVERRIDE||USER_CONCURRENCY||1);q.push(SINGLE_IP);})();return;}
213
+
214
+ // === Lancement daemon ===
193
215
  const concurrency = CORE_OVERRIDE||USER_CONCURRENCY||os.cpus().length||1;
194
216
  const q = new ScanQueue(concurrency);
195
217
  log(`Fail2Scan started. Watching ${LOG_PATH} -> output ${OUT_ROOT}, concurrency ${concurrency}`);
196
218
 
197
- // -------------------- FIX : regex BAN correct --------------------
198
- const BAN_RE = /Ban\s+(\d{1,3}(?:\.\d{1,3}){3})/;
199
-
219
+ const BAN_RE = /\bBan\b/i;
200
220
  class FileTail{
201
221
  constructor(filePath,onLine){this.filePath=filePath;this.onLine=onLine;this.pos=0;this.inode=null;this.buf='';this.watch=null;this.start();}
202
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(()=>{});}
@@ -204,18 +224,7 @@ class FileTail{
204
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{}}
205
225
  close(){try{this.watch?.close();}catch{}}
206
226
  }
207
-
208
- const tail=new FileTail(LOG_PATH,line=>{
209
- try{
210
- const m=line.match(BAN_RE);
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
- });
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);}});
219
228
 
220
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();}
221
230
  process.on('SIGINT',shutdown);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roflsec/fail2scan",
3
- "version": "0.0.12",
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"