Cloudflare-WAF-To-AbuseIPDB/index.js
2024-08-15 10:31:55 +02:00

232 lines
No EOL
8.4 KiB
JavaScript

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));
}
})();