232 lines
No EOL
8.4 KiB
JavaScript
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));
|
|
}
|
|
})(); |