require('dotenv').config(); const axios = require('axios'); const fs = require('fs'); const path = require('path'); const { CLOUDFLARE_ZONE_ID, CLOUDFLARE_EMAIL, CLOUDFLARE_API_KEY, ABUSEIPDB_API_KEY, NODE_ENV } = process.env; const CSV_FILE_PATH = path.join(__dirname, 'reported_ips.csv'); const TIME_WINDOW_MS = 20 * 60 * 1000; const COOLDOWN_MS = 1000; const BLOCK_TIME_MS = 5 * 60 * 60 * 1000; // 5h if (!fs.existsSync(CSV_FILE_PATH)) { fs.writeFileSync(CSV_FILE_PATH, 'Timestamp,RayID,IP,Endpoint,Action,Country\n'); } const PAYLOAD = { query: `query ListFirewallEvents($zoneTag: string, $filter: FirewallEventsAdaptiveFilter_InputObject) { viewer { zones(filter: { zoneTag: $zoneTag }) { firewallEventsAdaptive( filter: $filter limit: 1000 orderBy: [datetime_DESC] ) { action clientASNDescription clientAsn clientCountryName clientIP clientRequestHTTPHost clientRequestHTTPMethodName clientRequestHTTPProtocol clientRequestPath clientRequestQuery datetime rayName ruleId source userAgent } } } }`, variables: { zoneTag: CLOUDFLARE_ZONE_ID, filter: { datetime_geq: new Date(Date.now() - (60 * 60 * 10.5 * 1000)).toISOString(), datetime_leq: new Date(Date.now() - (60 * 60 * 8 * 1000)).toISOString(), AND: [ { action_neq: 'allow' }, { action_neq: 'skip' }, { action_neq: 'challenge_solved' }, { action_neq: 'challenge_failed' }, { action_neq: 'challenge_bypassed' }, { action_neq: 'jschallenge_solved' }, { action_neq: 'jschallenge_failed' }, { action_neq: 'jschallenge_bypassed' }, { action_neq: 'managed_challenge_skipped' }, { action_neq: 'managed_challenge_non_interactive_solved' }, { action_neq: 'managed_challenge_interactive_solved' }, { action_neq: 'managed_challenge_bypassed' } ] } } }; const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${CLOUDFLARE_API_KEY}`, 'X-Auth-Email': CLOUDFLARE_EMAIL }; const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.svg', '.webp']; const isImageRequest = loc => imageExtensions.some(ext => loc.toLowerCase().endsWith(ext)); const logToCSV = (timestamp, rayid, ip, endpoint, action, country) => { const logLine = `${timestamp.toISOString()},${rayid},${ip},${endpoint},${action},${country}\n`; fs.appendFileSync(CSV_FILE_PATH, logLine); }; const logMessage = (level, message) => { const logLevels = { info: '[INFO]', warn: '[WARN]', error: '[FAIL]' }; const timestamp = NODE_ENV === 'development' ? `${new Date().toISOString()}: ` : ''; console[level](`${logLevels[level]} ${timestamp}${message}`); }; const getBlockedIP = async () => { try { const { data } = await axios.post('https://api.cloudflare.com/client/v4/graphql/', PAYLOAD, { headers }); logMessage('info', `Fetched ${data.data.viewer.zones[0].firewallEventsAdaptive.length} events from Cloudflare`); return data; } catch (err) { if (err.response) { logMessage('error', `HTTP Error: ${err.response.status}. Data: ${JSON.stringify(err.response.data, null, 2)}`); } else if (err.request) { logMessage('error', 'No response received from Cloudflare.'); } else { logMessage('error', `Request setup error: ${err.message}`); } return null; } }; const getComment = (it) => ` IP: ${it.clientIP} triggered Cloudflare WAF. Action: ${it.action} Source: ${it.source} Client ASN: ${it.clientAsn} Client ASN Description: ${it.clientASNDescription} Client Country: ${it.clientCountryName} HTTP Host: ${it.clientRequestHTTPHost} HTTP Method: ${it.clientRequestHTTPMethodName} HTTP Protocol: ${it.clientRequestHTTPProtocol} HTTP Path: ${it.clientRequestPath} HTTP Query: ${it.clientRequestQuery} Datetime: ${it.datetime} Ray ID: ${it.rayName} Rule ID: ${it.ruleId} User Agent: ${it.userAgent} Report generated by Node-Cloudflare-WAF-To-AbuseIPDB (https://github.com/sefinek24/Node-Cloudflare-WAF-To-AbuseIPDB). `; const reportBadIP = async (it, skippedRayIds, blockedIPs) => { const endpoint = `${it.clientRequestHTTPHost}${it.clientRequestPath}`; const country = it.clientCountryName; if (isImageRequest(it.clientRequestPath)) { skippedRayIds.add(it.rayName); logToCSV(new Date(), it.rayName, it.clientIP, endpoint, 'Skipped - Image Request', country); logMessage('info', `Skipping IP: ${it.clientIP} for domain: ${it.clientRequestHTTPHost}, Endpoint: ${endpoint} (Image request detected)`); return false; } try { const url = 'https://api.abuseipdb.com/api/v2/report'; const params = { ip: it.clientIP, categories: '19', comment: getComment(it) }; await axios.post(url, params, { headers: { 'Accept': 'application/json', 'Key': ABUSEIPDB_API_KEY } }); logToCSV(new Date(), it.rayName, it.clientIP, endpoint, 'Reported', country); logMessage('info', `Successfully reported IP: ${it.clientIP} for domain: ${it.clientRequestHTTPHost}, Endpoint: ${endpoint}`); return true; } catch (err) { if (err.response && err.response.status === 429) { blockedIPs.set(it.clientIP, Date.now()); logToCSV(new Date(), it.rayName, it.clientIP, endpoint, 'Blocked - 429 Too Many Requests', country); logMessage('warn', `Rate limited (429) while reporting IP: ${it.clientIP}, Domain: ${it.clientRequestHTTPHost}, Endpoint: ${endpoint}. Will retry after 5 hours.`); } else { logMessage('error', `${err.message} - IP: ${it.clientIP}; Domain: ${it.clientRequestHTTPHost}; Endpoint: ${endpoint}`); } return false; } }; const exceptedRuleId = new Set(['fa01280809254f82978e827892db4e46']); const shouldReportDomain = (domain, reportedIPs) => { const lastReport = reportedIPs.find(entry => entry.domain === domain); if (!lastReport) return true; const timeSinceLastReport = Date.now() - lastReport.timestamp.getTime(); return timeSinceLastReport > TIME_WINDOW_MS; }; const shouldSkipBlockedIP = (ip, blockedIPs) => { const lastBlock = blockedIPs.get(ip); if (!lastBlock) return false; const timeSinceLastBlock = Date.now() - lastBlock; return timeSinceLastBlock < BLOCK_TIME_MS; }; const readReportedIPs = () => { if (!fs.existsSync(CSV_FILE_PATH)) return []; const content = fs.readFileSync(CSV_FILE_PATH, 'utf8'); return content.split('\n').slice(1).map(line => { const [timestamp, rayid, ip, domain, action, country] = line.split(','); return { timestamp: new Date(timestamp), rayid, ip, domain, action, country }; }).filter(entry => entry.timestamp && entry.rayid && entry.ip); }; (async () => { logMessage('info', 'Starting IP reporting process'); const reportedIPs = readReportedIPs(); const skippedRayIds = new Set(reportedIPs.filter(ip => ip.action.startsWith('Skipped')).map(ip => ip.rayid)); const blockedIPs = new Map(reportedIPs.filter(ip => ip.action.includes('429')).map(ip => [ip.ip, ip.timestamp.getTime()])); while (true) { logMessage('info', '===================== New Reporting Cycle ====================='); const data = await getBlockedIP(); if (data && data.data) { const ipBadList = data.data.viewer.zones[0].firewallEventsAdaptive; for (const i of ipBadList) { const endpoint = `${i.clientRequestHTTPHost}${i.clientRequestPath}`; if (skippedRayIds.has(i.rayName) || shouldSkipBlockedIP(i.clientIP, blockedIPs)) { continue; } if (!exceptedRuleId.has(i.ruleId) && shouldReportDomain(i.clientRequestHTTPHost, reportedIPs)) { const reported = await reportBadIP(i, skippedRayIds, blockedIPs); if (reported) { await new Promise(resolve => setTimeout(resolve, COOLDOWN_MS)); } } else if (!skippedRayIds.has(i.rayName)) { skippedRayIds.add(i.rayName); logToCSV(new Date(), i.rayName, i.clientIP, endpoint, 'Skipped - Already Reported', i.clientCountryName); logMessage('info', `Skipping IP: ${i.clientIP} for domain: ${i.clientRequestHTTPHost}, Endpoint: ${endpoint} (Already reported recently)`); } } } logMessage('info', '==================== End of Reporting Cycle ===================='); await new Promise(resolve => setTimeout(resolve, NODE_ENV === 'production' ? 2 * 60 * 60 * 1000 : 10 * 1000)); } })();