@roflsec/fail2scan 0.0.5 → 0.0.7
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 +28 -13
- package/bin/old.daemon.js +147 -293
- package/package.json +5 -1
package/bin/daemon.js
CHANGED
|
@@ -4,6 +4,24 @@ const fs = require('fs'), path = require('path'), os = require('os');
|
|
|
4
4
|
const { execFile, spawn } = require('child_process');
|
|
5
5
|
const { promisify } = require('util');
|
|
6
6
|
const execFileP = promisify(execFile);
|
|
7
|
+
require('dotenv').config({ quiet:true})
|
|
8
|
+
|
|
9
|
+
// -------------------- IPGeolocation --------------------
|
|
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
|
+
|
|
16
|
+
getGeo = (ip) => {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
const params = new GeolocationParams();
|
|
19
|
+
params.setIPAddress(ip);
|
|
20
|
+
params.setFields("geo,time_zone,currency,asn,security");
|
|
21
|
+
ipGeo.getGeolocation((res) => resolve(res), params);
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
}
|
|
7
25
|
|
|
8
26
|
// -------------------- CLI / CONFIG --------------------
|
|
9
27
|
const argv = process.argv.slice(2);
|
|
@@ -67,15 +85,12 @@ function spawnOneNmap(args, outFile) {
|
|
|
67
85
|
}
|
|
68
86
|
|
|
69
87
|
async function spawnNmapParallel(ip, outDir, requestedArgs, parts) {
|
|
70
|
-
// requestedArgs: array of args
|
|
71
|
-
// if -p- not present, run single nmap
|
|
72
88
|
if (!requestedArgs.includes('-p-')) {
|
|
73
89
|
const outNmap = path.join(outDir, 'nmap.txt');
|
|
74
90
|
await spawnOneNmap([...requestedArgs, ip], outNmap);
|
|
75
91
|
return;
|
|
76
92
|
}
|
|
77
93
|
|
|
78
|
-
// decide number of parts
|
|
79
94
|
const cpuCount = os.cpus() ? os.cpus().length : 1;
|
|
80
95
|
const numParts = Math.max(1, parts || CORE_OVERRIDE || cpuCount);
|
|
81
96
|
|
|
@@ -88,15 +103,12 @@ async function spawnNmapParallel(ip, outDir, requestedArgs, parts) {
|
|
|
88
103
|
try { fs.mkdirSync(subdir, { recursive: true, mode: 0o750 }); } catch (e) {}
|
|
89
104
|
const outNmap = path.join(subdir, 'nmap.txt');
|
|
90
105
|
const portArg = `-p${start}-${end}`;
|
|
91
|
-
// replace '-p-' with '-pstart-end' in requestedArgs
|
|
92
106
|
const args = requestedArgs.map(a => a === '-p-' ? portArg : a).concat([ip]);
|
|
93
107
|
jobs.push(spawnOneNmap(args, outNmap));
|
|
94
108
|
}
|
|
95
109
|
|
|
96
|
-
// await all parts
|
|
97
110
|
await Promise.all(jobs);
|
|
98
111
|
|
|
99
|
-
// merge outputs into single nmap.txt (preserve order)
|
|
100
112
|
const partsContent = [];
|
|
101
113
|
for (let i = 0; i < numParts; i++) {
|
|
102
114
|
const fn = path.join(outDir, `part-${i}`, 'nmap.txt');
|
|
@@ -107,18 +119,11 @@ async function spawnNmapParallel(ip, outDir, requestedArgs, parts) {
|
|
|
107
119
|
try { fs.writeFileSync(path.join(outDir, 'nmap.txt'), partsContent.join('\n\n--- PART ---\n\n')); } catch (e) {}
|
|
108
120
|
}
|
|
109
121
|
|
|
110
|
-
// wrapper used by performScan
|
|
111
122
|
async function runNmap(ip, outDir, requestedArgs) {
|
|
112
|
-
// adapt -sS -> -sT if not root
|
|
113
123
|
const isRoot = (typeof process.getuid === 'function' && process.getuid() === 0);
|
|
114
124
|
const args = requestedArgs.map(a => (!isRoot && a === '-sS') ? '-sT' : a);
|
|
115
|
-
|
|
116
|
-
// if user explicitly set a small host-timeout, keep it; otherwise let parallel jobs share same requested args
|
|
117
|
-
// decide parts from CORE_OVERRIDE or cpu
|
|
118
125
|
const cpuCount = os.cpus() ? os.cpus().length : 1;
|
|
119
126
|
const parts = CORE_OVERRIDE || cpuCount;
|
|
120
|
-
|
|
121
|
-
// if args include -p- -> use parallel split, else single
|
|
122
127
|
await spawnNmapParallel(ip, outDir, args, parts);
|
|
123
128
|
}
|
|
124
129
|
|
|
@@ -147,6 +152,16 @@ async function performScan(ip){
|
|
|
147
152
|
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 = []; }
|
|
148
153
|
|
|
149
154
|
try{ fs.writeFileSync(path.join(outDir,'summary.json'),JSON.stringify(summary,null,2)); } catch (e) {}
|
|
155
|
+
|
|
156
|
+
// geo non bloquant
|
|
157
|
+
if(process.env.IPGEO_API_KEY){
|
|
158
|
+
getGeo(ip)
|
|
159
|
+
.then(geo => {
|
|
160
|
+
try{ fs.writeFileSync(path.join(outDir,'geo.json'), JSON.stringify(geo, null, 2)); } catch(e){}
|
|
161
|
+
})
|
|
162
|
+
.catch(e=>{});
|
|
163
|
+
}
|
|
164
|
+
|
|
150
165
|
try{ fs.chmodSync(outDir,0o750); } catch (e) {}
|
|
151
166
|
log('Scan written for',ip,'->',outDir);
|
|
152
167
|
}
|
package/bin/old.daemon.js
CHANGED
|
@@ -1,346 +1,200 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Fail2Scan daemon - integrated version
|
|
6
|
-
* - watches fail2ban log for "Ban" events (handles rotation)
|
|
7
|
-
* - extracts IPv4/IPv6
|
|
8
|
-
* - queue with persistence and rescan TTL
|
|
9
|
-
* - fallback output dir if /var/log/fail2scan not writable
|
|
10
|
-
* - single-ip CLI --scan-ip
|
|
11
|
-
*
|
|
12
|
-
* Usage:
|
|
13
|
-
* fail2scan-daemon --log /var/log/fail2ban.log --out /var/log/fail2scan --concurrency 2 --nmap-args "-sS -Pn -p- -T4 -sV" --quiet
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
const fs = require('fs');
|
|
17
|
-
const path = require('path');
|
|
18
|
-
const os = require('os');
|
|
3
|
+
const fs = require('fs'), path = require('path'), os = require('os');
|
|
19
4
|
const { execFile, spawn } = require('child_process');
|
|
20
5
|
const { promisify } = require('util');
|
|
21
6
|
const execFileP = promisify(execFile);
|
|
22
7
|
|
|
23
8
|
// -------------------- CLI / CONFIG --------------------
|
|
24
9
|
const argv = process.argv.slice(2);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
const SINGLE_IP = getArg('--scan-ip', null);
|
|
10
|
+
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; };
|
|
11
|
+
if(argv.includes('--help')||argv.includes('-h')){console.log(`Fail2Scan optimized daemon
|
|
12
|
+
--log PATH (default /var/log/fail2ban.log)
|
|
13
|
+
--out PATH (default /var/log/fail2scan)
|
|
14
|
+
--concurrency N (default 1)
|
|
15
|
+
--cores N (override concurrency with CPU cores)
|
|
16
|
+
--nmap-args "args" (default "-sS -Pn -p- -T4 -sV")
|
|
17
|
+
--scan-ip IP (do one scan and exit)
|
|
18
|
+
--quiet
|
|
19
|
+
`); process.exit(0); }
|
|
20
|
+
|
|
21
|
+
const LOG_PATH = getArg('--log','/var/log/fail2ban.log');
|
|
22
|
+
const OUT_ROOT = getArg('--out','/var/log/fail2scan');
|
|
23
|
+
const USER_CONCURRENCY = parseInt(getArg('--concurrency','0'),10)||0;
|
|
24
|
+
const CORE_OVERRIDE = parseInt(getArg('--cores','0'),10)||0;
|
|
25
|
+
const NMAP_ARGS_STR = getArg('--nmap-args','-sS -Pn -p- -T4 -sV');
|
|
26
|
+
const SINGLE_IP = getArg('--scan-ip',null);
|
|
43
27
|
const QUIET = argv.includes('--quiet');
|
|
28
|
+
const RESCAN_TTL_SEC = 60*60;
|
|
29
|
+
const STATE_FILE = path.join(os.homedir(),'.fail2scan_state.json');
|
|
30
|
+
const LOG_FILE = path.join(os.homedir(),'.fail2scan.log');
|
|
44
31
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// -------------------- small logger to file --------------------
|
|
48
|
-
const STATE_FILE = path.join(os.homedir(), '.fail2scan_state.json');
|
|
49
|
-
const LOG_FILE = path.join(os.homedir(), '.fail2scan.log');
|
|
50
|
-
function appendLog(msg) {
|
|
51
|
-
try { fs.appendFileSync(LOG_FILE, new Date().toISOString() + ' ' + msg + '\n'); } catch (e) {}
|
|
52
|
-
}
|
|
32
|
+
const log=(...a)=>{if(!QUIET)console.log(new Date().toISOString(),...a); try{fs.appendFileSync(LOG_FILE,new Date().toISOString()+' '+a.join(' ')+'\n');}catch{}};
|
|
53
33
|
|
|
54
34
|
// -------------------- utilities --------------------
|
|
55
|
-
|
|
56
|
-
async function which(bin)
|
|
57
|
-
async function runCmdCapture(cmd,
|
|
58
|
-
|
|
59
|
-
const { stdout, stderr } = await execFileP(cmd, args, { maxBuffer: 1024 * 1024 * 32, ...opts });
|
|
60
|
-
return { ok: true, stdout: stdout || '', stderr: stderr || '' };
|
|
61
|
-
} catch (e) {
|
|
62
|
-
return { ok: false, stdout: (e.stdout || '') + '', stderr: (e.stderr || e.message) + '' };
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
function safeMkdirSyncWithFallback(p) {
|
|
66
|
-
try { fs.mkdirSync(p, { recursive: true, mode: 0o750 }); return p; }
|
|
67
|
-
catch (e) {
|
|
68
|
-
const fallback = path.join('/tmp', 'fail2scan');
|
|
69
|
-
try { fs.mkdirSync(fallback, { recursive: true, mode: 0o750 }); return fallback; }
|
|
70
|
-
catch (ee) { throw e; }
|
|
71
|
-
}
|
|
72
|
-
}
|
|
35
|
+
const sanitizeFilename=s=>String(s).replace(/[:\/\\<>?"|* ]+/g,'_');
|
|
36
|
+
async function which(bin){try{await execFileP('which',[bin]);return true;}catch{return false;}}
|
|
37
|
+
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)+''};}}
|
|
38
|
+
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;}}}
|
|
73
39
|
|
|
74
40
|
// -------------------- prerequisites --------------------
|
|
75
|
-
(async
|
|
76
|
-
const tools = ['nmap', 'dig', 'whois', 'which'];
|
|
77
|
-
for (const t of tools) {
|
|
78
|
-
if (t === 'which') continue;
|
|
79
|
-
if (!(await which(t))) {
|
|
80
|
-
console.error(`Missing required binary: ${t}. Please install it (eg: apt install ${t}).`);
|
|
81
|
-
process.exit(2);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
})().catch(e => { console.error('Prereq check failed', e); process.exit(2); });
|
|
41
|
+
(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);});
|
|
85
42
|
|
|
86
|
-
// --------------------
|
|
87
|
-
function loadState() {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const raw = fs.readFileSync(STATE_FILE, 'utf8');
|
|
91
|
-
const j = JSON.parse(raw);
|
|
92
|
-
return {
|
|
93
|
-
seen: new Set(Array.isArray(j.seen) ? j.seen : []),
|
|
94
|
-
retryAfter: typeof j.retryAfter === 'object' ? j.retryAfter : {}
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
} catch (e) {}
|
|
98
|
-
return { seen: new Set(), retryAfter: {} };
|
|
99
|
-
}
|
|
100
|
-
function saveState(state) {
|
|
101
|
-
try {
|
|
102
|
-
const obj = { seen: Array.from(state.seen || []), retryAfter: state.retryAfter || {} };
|
|
103
|
-
const dir = path.dirname(STATE_FILE);
|
|
104
|
-
try { fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); } catch (e) {}
|
|
105
|
-
fs.writeFileSync(STATE_FILE, JSON.stringify(obj, null, 2));
|
|
106
|
-
} catch (e) {}
|
|
107
|
-
}
|
|
108
|
-
const STATE = loadState();
|
|
109
|
-
const RESCAN_TTL_SEC = 60 * 60; // default 1 hour
|
|
43
|
+
// -------------------- state --------------------
|
|
44
|
+
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:{}};}
|
|
45
|
+
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{}}
|
|
46
|
+
const STATE=loadState();
|
|
110
47
|
|
|
111
48
|
// -------------------- IP extraction --------------------
|
|
112
|
-
function extractIpFromLine(line) {
|
|
113
|
-
// prefer strict IPv4
|
|
114
|
-
const ipv4 = line.match(/\b(?:\d{1,3}\.){3}\d{1,3}\b/);
|
|
115
|
-
if (ipv4 && ipv4[0]) return ipv4[0];
|
|
116
|
-
// simple IPv6 match
|
|
117
|
-
const ipv6 = line.match(/\b([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}\b/);
|
|
118
|
-
if (ipv6 && ipv6[0]) return ipv6[0];
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
49
|
+
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;}
|
|
121
50
|
|
|
122
|
-
// --------------------
|
|
123
|
-
function
|
|
51
|
+
// -------------------- nmap helpers (parallel support) --------------------
|
|
52
|
+
function spawnOneNmap(args, outFile) {
|
|
124
53
|
return new Promise((resolve, reject) => {
|
|
125
|
-
const outNmap = path.join(outDir, 'nmap.txt');
|
|
126
|
-
const args = nmapArgs.concat([ip]);
|
|
127
54
|
const proc = spawn('nmap', args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
128
|
-
const outStream = fs.createWriteStream(
|
|
55
|
+
const outStream = fs.createWriteStream(outFile, { flags: 'w' });
|
|
129
56
|
proc.stdout.pipe(outStream);
|
|
130
57
|
let stderr = '';
|
|
131
58
|
proc.stderr.on('data', c => { stderr += c.toString(); });
|
|
132
59
|
proc.on('close', code => {
|
|
133
|
-
if (stderr)
|
|
60
|
+
if (stderr) {
|
|
61
|
+
try { fs.appendFileSync(outFile, '\n\nSTDERR:\n' + stderr); } catch (e) {}
|
|
62
|
+
}
|
|
134
63
|
resolve({ code, ok: code === 0 });
|
|
135
64
|
});
|
|
136
65
|
proc.on('error', err => reject(err));
|
|
137
66
|
});
|
|
138
67
|
}
|
|
139
68
|
|
|
140
|
-
async function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
outDir = path.join(safeMkdirSyncWithFallback(path.dirname(outDir)), path.basename(outDir));
|
|
147
|
-
fs.mkdirSync(outDir, { recursive: true, mode: 0o750 });
|
|
148
|
-
} catch (e) {
|
|
149
|
-
log('Cannot create out dir for', ip, '-', e.message);
|
|
150
|
-
// schedule quick retry and exit
|
|
151
|
-
STATE.retryAfter[ip] = Math.floor(Date.now() / 1000) + 60;
|
|
152
|
-
saveState(STATE);
|
|
69
|
+
async function spawnNmapParallel(ip, outDir, requestedArgs, parts) {
|
|
70
|
+
// requestedArgs: array of args
|
|
71
|
+
// if -p- not present, run single nmap
|
|
72
|
+
if (!requestedArgs.includes('-p-')) {
|
|
73
|
+
const outNmap = path.join(outDir, 'nmap.txt');
|
|
74
|
+
await spawnOneNmap([...requestedArgs, ip], outNmap);
|
|
153
75
|
return;
|
|
154
76
|
}
|
|
155
77
|
|
|
156
|
-
|
|
78
|
+
// decide number of parts
|
|
79
|
+
const cpuCount = os.cpus() ? os.cpus().length : 1;
|
|
80
|
+
const numParts = Math.max(1, parts || CORE_OVERRIDE || cpuCount);
|
|
81
|
+
|
|
82
|
+
const portsPer = Math.floor(65535 / numParts);
|
|
83
|
+
const jobs = [];
|
|
84
|
+
for (let i = 0; i < numParts; i++) {
|
|
85
|
+
const start = 1 + i * portsPer;
|
|
86
|
+
const end = (i === numParts - 1) ? 65535 : ((i + 1) * portsPer);
|
|
87
|
+
const subdir = path.join(outDir, `part-${i}`);
|
|
88
|
+
try { fs.mkdirSync(subdir, { recursive: true, mode: 0o750 }); } catch (e) {}
|
|
89
|
+
const outNmap = path.join(subdir, 'nmap.txt');
|
|
90
|
+
const portArg = `-p${start}-${end}`;
|
|
91
|
+
// replace '-p-' with '-pstart-end' in requestedArgs
|
|
92
|
+
const args = requestedArgs.map(a => a === '-p-' ? portArg : a).concat([ip]);
|
|
93
|
+
jobs.push(spawnOneNmap(args, outNmap));
|
|
94
|
+
}
|
|
157
95
|
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
const isRoot = (typeof process.getuid === 'function' && process.getuid() === 0);
|
|
161
|
-
const nmapArgs = requested.map(a => (!isRoot && a === '-sS') ? '-sT' : a);
|
|
96
|
+
// await all parts
|
|
97
|
+
await Promise.all(jobs);
|
|
162
98
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
99
|
+
// merge outputs into single nmap.txt (preserve order)
|
|
100
|
+
const partsContent = [];
|
|
101
|
+
for (let i = 0; i < numParts; i++) {
|
|
102
|
+
const fn = path.join(outDir, `part-${i}`, 'nmap.txt');
|
|
103
|
+
try {
|
|
104
|
+
if (fs.existsSync(fn)) partsContent.push(fs.readFileSync(fn, 'utf8'));
|
|
105
|
+
} catch (e) {}
|
|
170
106
|
}
|
|
107
|
+
try { fs.writeFileSync(path.join(outDir, 'nmap.txt'), partsContent.join('\n\n--- PART ---\n\n')); } catch (e) {}
|
|
108
|
+
}
|
|
171
109
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
} catch (e) { summary.cmds.dig = { ok: false, err: e.message }; }
|
|
178
|
-
|
|
179
|
-
// whois
|
|
180
|
-
try {
|
|
181
|
-
const who = await runCmdCapture('whois', [ip]);
|
|
182
|
-
fs.writeFileSync(path.join(outDir, 'whois.txt'), (who.stdout || '') + (who.stderr ? '\n\nSTDERR:\n' + who.stderr : ''));
|
|
183
|
-
summary.cmds.whois = { ok: who.ok, path: 'whois.txt' };
|
|
184
|
-
} catch (e) { summary.cmds.whois = { ok: false, err: e.message }; }
|
|
110
|
+
// wrapper used by performScan
|
|
111
|
+
async function runNmap(ip, outDir, requestedArgs) {
|
|
112
|
+
// adapt -sS -> -sT if not root
|
|
113
|
+
const isRoot = (typeof process.getuid === 'function' && process.getuid() === 0);
|
|
114
|
+
const args = requestedArgs.map(a => (!isRoot && a === '-sS') ? '-sT' : a);
|
|
185
115
|
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
summary.open_ports = open;
|
|
191
|
-
} catch (e) { summary.open_ports = []; }
|
|
116
|
+
// if user explicitly set a small host-timeout, keep it; otherwise let parallel jobs share same requested args
|
|
117
|
+
// decide parts from CORE_OVERRIDE or cpu
|
|
118
|
+
const cpuCount = os.cpus() ? os.cpus().length : 1;
|
|
119
|
+
const parts = CORE_OVERRIDE || cpuCount;
|
|
192
120
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
log('Scan written for', ip, '->', outDir);
|
|
121
|
+
// if args include -p- -> use parallel split, else single
|
|
122
|
+
await spawnNmapParallel(ip, outDir, args, parts);
|
|
196
123
|
}
|
|
197
124
|
|
|
198
|
-
// --------------------
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
125
|
+
// -------------------- scan --------------------
|
|
126
|
+
async function performScan(ip){
|
|
127
|
+
const now=new Date(),dateDir=now.toISOString().slice(0,10),safeIp=sanitizeFilename(ip);
|
|
128
|
+
let outDir=path.join(OUT_ROOT,dateDir,`${safeIp}_${now.toISOString().replace(/[:.]/g,'-')}`);
|
|
129
|
+
outDir=path.join(safeMkdirSyncWithFallback(path.dirname(outDir)),path.basename(outDir));
|
|
130
|
+
try { fs.mkdirSync(outDir,{recursive:true,mode:0o750}); } catch (e) {}
|
|
131
|
+
const summary={ip,ts:now.toISOString(),cmds:{}};
|
|
132
|
+
|
|
133
|
+
const requested=NMAP_ARGS_STR.trim().split(/\s+/).filter(Boolean);
|
|
134
|
+
|
|
135
|
+
try{
|
|
136
|
+
log('Running nmap on',ip,'args:',requested.join(' '));
|
|
137
|
+
await runNmap(ip, outDir, requested);
|
|
138
|
+
summary.cmds.nmap = { ok: true, args: requested.join(' '), path: 'nmap.txt' };
|
|
139
|
+
} catch(e){
|
|
140
|
+
log('nmap failed for',ip, e && e.message ? e.message : e);
|
|
141
|
+
summary.cmds.nmap = { ok: false, err: e && e.message ? e.message : String(e) };
|
|
205
142
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
143
|
+
|
|
144
|
+
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) }; }
|
|
145
|
+
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) }; }
|
|
146
|
+
|
|
147
|
+
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 = []; }
|
|
148
|
+
|
|
149
|
+
try{ fs.writeFileSync(path.join(outDir,'summary.json'),JSON.stringify(summary,null,2)); } catch (e) {}
|
|
150
|
+
try{ fs.chmodSync(outDir,0o750); } catch (e) {}
|
|
151
|
+
log('Scan written for',ip,'->',outDir);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// -------------------- queue optimized --------------------
|
|
155
|
+
class ScanQueue{
|
|
156
|
+
constructor(concurrency=1){this.concurrency=concurrency;this.running=0;this.q=[];this.set=STATE.seen;this.tmpCache=new Set();}
|
|
157
|
+
push(ip){
|
|
158
|
+
const now=Math.floor(Date.now()/1000),next=STATE.retryAfter[ip]||0;
|
|
159
|
+
if((this.set.has(ip)&&next>now)||this.tmpCache.has(ip)){log('IP already queued or running (TTL/cache):',ip);return;}
|
|
160
|
+
if(this.set.has(ip)&&next<=now)log('Re-queueing after TTL:',ip),this.set.delete(ip);
|
|
161
|
+
this.set.add(ip);STATE.seen=this.set;saveState(STATE);
|
|
162
|
+
this.q.push(ip);this.tmpCache.add(ip);this._next();
|
|
222
163
|
}
|
|
223
|
-
_next()
|
|
224
|
-
if
|
|
225
|
-
const ip
|
|
226
|
-
if (!ip) return;
|
|
164
|
+
_next(){
|
|
165
|
+
if(this.running>=this.concurrency)return;
|
|
166
|
+
const ip=this.q.shift();if(!ip)return;
|
|
227
167
|
this.running++;
|
|
228
|
-
(async
|
|
229
|
-
try
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
} catch (e) {
|
|
234
|
-
log('Error scanning', ip, e && e.message ? e.message : e);
|
|
235
|
-
} finally {
|
|
236
|
-
STATE.retryAfter[ip] = Math.floor(Date.now() / 1000) + RESCAN_TTL_SEC;
|
|
168
|
+
(async()=>{
|
|
169
|
+
try{log('Scanning',ip);await performScan(ip);log('Done',ip);}
|
|
170
|
+
catch(e){log('Error scanning',ip,e.message||e);}
|
|
171
|
+
finally{
|
|
172
|
+
STATE.retryAfter[ip]=Math.floor(Date.now()/1000)+RESCAN_TTL_SEC;
|
|
237
173
|
saveState(STATE);
|
|
238
|
-
this.set.delete(ip);
|
|
239
|
-
this.running--;
|
|
240
|
-
setImmediate(() => this._next());
|
|
174
|
+
this.set.delete(ip);this.tmpCache.delete(ip);
|
|
175
|
+
this.running--;setImmediate(()=>this._next());
|
|
241
176
|
}
|
|
242
177
|
})();
|
|
178
|
+
setImmediate(()=>this._next());
|
|
243
179
|
}
|
|
244
180
|
}
|
|
245
181
|
|
|
246
|
-
// --------------------
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
this.pos = 0;
|
|
252
|
-
this.inode = null;
|
|
253
|
-
this.buf = '';
|
|
254
|
-
this.watch = null;
|
|
255
|
-
this.start();
|
|
256
|
-
}
|
|
257
|
-
start() {
|
|
258
|
-
try {
|
|
259
|
-
const st = fs.statSync(this.filePath);
|
|
260
|
-
this.inode = st.ino;
|
|
261
|
-
this.pos = st.size;
|
|
262
|
-
} catch (e) {
|
|
263
|
-
this.inode = null;
|
|
264
|
-
this.pos = 0;
|
|
265
|
-
}
|
|
266
|
-
this._watch();
|
|
267
|
-
// try initial read (if file exists and appended)
|
|
268
|
-
this._readNew().catch(()=>{});
|
|
269
|
-
}
|
|
270
|
-
_watch() {
|
|
271
|
-
try {
|
|
272
|
-
this.watch = fs.watch(this.filePath, { persistent: true }, async () => {
|
|
273
|
-
try {
|
|
274
|
-
let st;
|
|
275
|
-
try { st = fs.statSync(this.filePath); } catch { st = null; }
|
|
276
|
-
if (!st) { this.inode = null; this.pos = 0; return; }
|
|
277
|
-
if (this.inode !== null && st.ino !== this.inode) { // rotated
|
|
278
|
-
this.inode = st.ino;
|
|
279
|
-
this.pos = 0;
|
|
280
|
-
} else if (this.inode === null) {
|
|
281
|
-
this.inode = st.ino;
|
|
282
|
-
this.pos = 0;
|
|
283
|
-
}
|
|
284
|
-
await this._readNew();
|
|
285
|
-
} catch (e) { /* ignore transient */ }
|
|
286
|
-
});
|
|
287
|
-
} catch (e) { log('fs.watch failed:', e.message); }
|
|
288
|
-
}
|
|
289
|
-
async _readNew() {
|
|
290
|
-
try {
|
|
291
|
-
const st = fs.statSync(this.filePath);
|
|
292
|
-
if (st.size < this.pos) this.pos = 0;
|
|
293
|
-
if (st.size === this.pos) return;
|
|
294
|
-
const stream = fs.createReadStream(this.filePath, { start: this.pos, end: st.size - 1, encoding: 'utf8' });
|
|
295
|
-
for await (const chunk of stream) {
|
|
296
|
-
this.buf += chunk;
|
|
297
|
-
let idx;
|
|
298
|
-
while ((idx = this.buf.indexOf('\n')) >= 0) {
|
|
299
|
-
const line = this.buf.slice(0, idx);
|
|
300
|
-
this.buf = this.buf.slice(idx + 1);
|
|
301
|
-
if (line.trim()) this.onLine(line);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
this.pos = st.size;
|
|
305
|
-
} catch (e) { /* ignore */ }
|
|
306
|
-
}
|
|
307
|
-
close() { try { if (this.watch) this.watch.close(); } catch (e) {} }
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// -------------------- Main startup --------------------
|
|
311
|
-
if (SINGLE_IP) {
|
|
312
|
-
(async () => {
|
|
313
|
-
const q = new ScanQueue(CONCURRENCY);
|
|
314
|
-
q.push(SINGLE_IP);
|
|
315
|
-
})();
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const q = new ScanQueue(CONCURRENCY);
|
|
320
|
-
log('Fail2Scan started. Watching', LOG_PATH, ' -> output', OUT_ROOT, 'concurrency', CONCURRENCY);
|
|
182
|
+
// -------------------- main --------------------
|
|
183
|
+
if(SINGLE_IP){(async()=>{const q=new ScanQueue(CORE_OVERRIDE||USER_CONCURRENCY||1);q.push(SINGLE_IP);})();return;}
|
|
184
|
+
const concurrency = CORE_OVERRIDE||USER_CONCURRENCY||os.cpus().length||1;
|
|
185
|
+
const q = new ScanQueue(concurrency);
|
|
186
|
+
log(`Fail2Scan started. Watching ${LOG_PATH} -> output ${OUT_ROOT}, concurrency ${concurrency}`);
|
|
321
187
|
|
|
322
|
-
// On each new line, extract IP and push to queue if Ban detected
|
|
323
188
|
const BAN_RE = /\bBan\b/i;
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
} catch (e) { log('onLine handler error', e && e.message ? e.message : e); }
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
// graceful shutdown
|
|
334
|
-
function shutdown() {
|
|
335
|
-
log('Shutting down Fail2Scan...');
|
|
336
|
-
tail.close();
|
|
337
|
-
// wait briefly for running tasks
|
|
338
|
-
const start = Date.now();
|
|
339
|
-
const wait = () => {
|
|
340
|
-
if (q.running === 0 || Date.now() - start > 10000) process.exit(0);
|
|
341
|
-
setTimeout(wait, 500);
|
|
342
|
-
};
|
|
343
|
-
wait();
|
|
189
|
+
class FileTail{
|
|
190
|
+
constructor(filePath,onLine){this.filePath=filePath;this.onLine=onLine;this.pos=0;this.inode=null;this.buf='';this.watch=null;this.start();}
|
|
191
|
+
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(()=>{});}
|
|
192
|
+
_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);}}
|
|
193
|
+
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{}}
|
|
194
|
+
close(){try{this.watch?.close();}catch{}}
|
|
344
195
|
}
|
|
345
|
-
|
|
346
|
-
|
|
196
|
+
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);}});
|
|
197
|
+
|
|
198
|
+
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();}
|
|
199
|
+
process.on('SIGINT',shutdown);
|
|
200
|
+
process.on('SIGTERM',shutdown);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roflsec/fail2scan",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
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"
|
|
@@ -19,5 +19,9 @@
|
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"engines": {
|
|
21
21
|
"node": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"dotenv": "^17.2.3",
|
|
25
|
+
"ip-geolocation-api-javascript-sdk": "^2.0.1"
|
|
22
26
|
}
|
|
23
27
|
}
|