Add two-tier IP exemption system; fix scan button to use /api/exempt
- Add EXEMPT_FILE, readExemptions/saveExemptions/isExempt/addExemption/removeExemption - Filter exempt IPs from scan results (still monitored by fail2ban) - buildBanList() appends exempt entries with jail:'exempt' - API: POST /api/exempt, DELETE /api/exempt/:ip, GET /api/exemptions - Frontend: [EXEMPT] filter tab, exempt jail color, REMOVE/ARREST/THREAT actions - Scan card [WHITELIST] button → [EXEMPT] calling /api/exempt (not ignoreip) - Fix isWhitelisted() to read only jail.local with proper CIDR matching - Fix readIgnoreIP() regex: \s* → [ \t]* to prevent cross-line capture Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@ const JAIL_LOCAL = process.env.JAIL_LOCAL || '/etc/fail2ban/jail.local';
|
||||
const CF_SYNC = process.env.CF_SYNC || '/usr/local/bin/cloudflare-whitelist-sync.sh';
|
||||
const MANUAL_JAIL = process.env.MANUAL_JAIL || 'manual-bans';
|
||||
const BAN_HIST_FILE = process.env.BAN_HIST_FILE || '/data/ban-history.json';
|
||||
const EXEMPT_FILE = process.env.EXEMPT_FILE || '/data/exemptions.json';
|
||||
const DEFAULT_DAYS = 3;
|
||||
const ABUSE_KEY = process.env.ABUSEIPDB_API_KEY;
|
||||
const AUTOBAN_THR = 75;
|
||||
@@ -133,7 +134,7 @@ async function buildBanList() {
|
||||
return { ...b, score, country };
|
||||
});
|
||||
|
||||
// Append whitelist IPs as a virtual "jail"
|
||||
// Append fail2ban ignoreip as "whitelist" (trusted — f2b won't monitor)
|
||||
whitelist.forEach(ip => {
|
||||
data.push({
|
||||
ip, jail: 'whitelist', duration: -1,
|
||||
@@ -144,6 +145,17 @@ async function buildBanList() {
|
||||
});
|
||||
});
|
||||
|
||||
// Append scan exemptions (reviewed — hidden from scan, f2b still watches)
|
||||
readExemptions().forEach(({ ip, note }) => {
|
||||
data.push({
|
||||
ip, jail: 'exempt', duration: -1,
|
||||
banTime: null, unbanTime: null,
|
||||
score: abuseCache.get(ip)?.score ?? null,
|
||||
country: abuseCache.get(ip)?.country ?? null,
|
||||
note,
|
||||
});
|
||||
});
|
||||
|
||||
banCache = { data, ts: Date.now() };
|
||||
return data;
|
||||
}
|
||||
@@ -216,6 +228,32 @@ async function refreshBanHistory() {
|
||||
} catch (e) { console.error('ban history refresh:', e.message); }
|
||||
}
|
||||
|
||||
// ── Exemptions (scan-level: hide from scan results, fail2ban still watches) ───
|
||||
function readExemptions() {
|
||||
try {
|
||||
if (!fs.existsSync(EXEMPT_FILE)) return [];
|
||||
return JSON.parse(fs.readFileSync(EXEMPT_FILE, 'utf8'));
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
function saveExemptions(list) {
|
||||
fs.writeFileSync(EXEMPT_FILE, JSON.stringify(list, null, 2));
|
||||
}
|
||||
|
||||
function isExempt(ip) {
|
||||
return readExemptions().some(e => e.ip === ip);
|
||||
}
|
||||
|
||||
function addExemption(ip, note = '') {
|
||||
const list = readExemptions().filter(e => e.ip !== ip);
|
||||
list.push({ ip, note, addedAt: new Date().toISOString() });
|
||||
saveExemptions(list);
|
||||
}
|
||||
|
||||
function removeExemption(ip) {
|
||||
saveExemptions(readExemptions().filter(e => e.ip !== ip));
|
||||
}
|
||||
|
||||
// ── Nginx log scanner ─────────────────────────────────────────────────────────
|
||||
function isWhitelisted(ip) {
|
||||
const wl = readIgnoreIP();
|
||||
@@ -243,7 +281,7 @@ async function processLogFile(file, cutoff) {
|
||||
const ipM = line.match(/\[Client ([^\]]+)\]/);
|
||||
if (!ipM) return;
|
||||
const ip = ipM[1];
|
||||
if (isWhitelisted(ip)) return;
|
||||
if (isWhitelisted(ip) || isExempt(ip)) return;
|
||||
const stM = line.match(/\s(\d{3})\s/);
|
||||
if (!stM || stM[1] === '200') return;
|
||||
const parts = line.split(/\s+/);
|
||||
@@ -360,6 +398,20 @@ app.delete('/api/whitelist/:ip', async (req, res) => {
|
||||
catch (e) { res.status(500).send(e.message); }
|
||||
});
|
||||
|
||||
app.post('/api/exempt', (req, res) => {
|
||||
const { ip, note } = req.body;
|
||||
if (!ip) return res.status(400).send('ip required');
|
||||
try { addExemption(ip, note || ''); banCache = null; res.send(`${ip} exempted from scan.`); }
|
||||
catch (e) { res.status(500).send(e.message); }
|
||||
});
|
||||
|
||||
app.delete('/api/exempt/:ip', (req, res) => {
|
||||
try { removeExemption(req.params.ip); banCache = null; res.send(`${req.params.ip} removed from exemptions.`); }
|
||||
catch (e) { res.status(500).send(e.message); }
|
||||
});
|
||||
|
||||
app.get('/api/exemptions', (req, res) => res.json(readExemptions()));
|
||||
|
||||
app.get('/api/check-abuse/:ip', async (req, res) => {
|
||||
if (!ABUSE_KEY) return res.status(503).send('AbuseIPDB key not configured');
|
||||
try { res.json(await checkAbuse(req.params.ip)); }
|
||||
|
||||
Reference in New Issue
Block a user