This commit is contained in:
Sefinek 2024-08-16 15:17:06 +02:00
parent 2dfa84583f
commit ef4de87e0e
7 changed files with 167 additions and 154 deletions

195
index.js
View file

@ -1,169 +1,67 @@
require('dotenv').config(); require('dotenv').config();
const axios = require('axios'); const axios = require('axios');
const fs = require('fs'); const PAYLOAD = require('./scripts/payload.js');
const path = require('path'); const generateComment = require('./scripts/generateComment.js');
const isImageRequest = require('./scripts/isImageRequest.js');
const headers = require('./scripts/headers.js');
const { logToCSV, readReportedIPs } = require('./scripts/csv.js');
const log = require('./scripts/log.js');
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 TIME_WINDOW_MS = 20 * 60 * 1000;
const COOLDOWN_MS = 1000; const COOLDOWN_MS = 2000;
const BLOCK_TIME_MS = 5 * 60 * 60 * 1000; // 5h 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 () => { const getBlockedIP = async () => {
try { try {
const { data } = await axios.post('https://api.cloudflare.com/client/v4/graphql/', PAYLOAD, { headers }); const res = await axios.post('https://api.cloudflare.com/client/v4/graphql', PAYLOAD, { headers: headers.CLOUDFLARE });
logMessage('info', `Fetched ${data.data.viewer.zones[0].firewallEventsAdaptive.length} events from Cloudflare`); if (!res.data?.data) return log('error', `Failed to retrieve data from Cloudflare (status ${res.status}). Missing permissions? Check your token. The required permission is Zone.Analytics.Read.`);
return data;
log('info', `Fetched ${res.data.data.viewer.zones[0].firewallEventsAdaptive.length} events from Cloudflare`);
return res.data;
} catch (err) { } catch (err) {
if (err.response) { if (err.response) {
logMessage('error', `HTTP Error: ${err.response.status}. Data: ${JSON.stringify(err.response.data, null, 2)}`); log('error', `${err.response.status} HTTP ERROR (api.cloudflare.com)\n${JSON.stringify(err.response.data, null, 2)}`);
} else if (err.request) { } else if (err.request) {
logMessage('error', 'No response received from Cloudflare.'); log('error', 'No response received from Cloudflare');
} else { } else {
logMessage('error', `Request setup error: ${err.message}`); log('error', `Unknown error with api.cloudflare.com. ${err.message}`);
} }
return null; 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 reportBadIP = async (it, skippedRayIds, blockedIPs) => {
const endpoint = `${it.clientRequestHTTPHost}${it.clientRequestPath}`; const url = `${it.clientRequestHTTPHost}${it.clientRequestPath}`;
const country = it.clientCountryName; const country = it.clientCountryName;
if (isImageRequest(it.clientRequestPath)) { if (isImageRequest(it.clientRequestPath)) {
skippedRayIds.add(it.rayName); skippedRayIds.add(it.rayName);
logToCSV(new Date(), it.rayName, it.clientIP, endpoint, 'Skipped - Image Request', country); logToCSV(new Date(), it.rayName, it.clientIP, url, 'Skipped - Image Request', country);
logMessage('info', `Skipping IP: ${it.clientIP} for domain: ${it.clientRequestHTTPHost}, Endpoint: ${endpoint} (Image request detected)`); log('info', `Skipping: ${it.clientIP}; URL: ${url}; (Image request detected)`);
return false; return false;
} }
try { try {
const url = 'https://api.abuseipdb.com/api/v2/report'; await axios.post('https://api.abuseipdb.com/api/v2/report', {
const params = {
ip: it.clientIP, ip: it.clientIP,
categories: '19', categories: '19',
comment: getComment(it) comment: generateComment(it)
}; }, { headers: headers.ABUSEIPDB });
await axios.post(url, params, { headers: { 'Accept': 'application/json', 'Key': ABUSEIPDB_API_KEY } }); logToCSV(new Date(), it.rayName, it.clientIP, url, 'Reported', country);
logToCSV(new Date(), it.rayName, it.clientIP, endpoint, 'Reported', country); log('info', `Reported: ${it.clientIP}; URL: ${url}`);
logMessage('info', `Successfully reported IP: ${it.clientIP} for domain: ${it.clientRequestHTTPHost}, Endpoint: ${endpoint}`);
return true; return true;
} catch (err) { } catch (err) {
if (err.response && err.response.status === 429) { if (err.response && err.response.status === 429) {
blockedIPs.set(it.clientIP, Date.now()); blockedIPs.set(it.clientIP, Date.now());
logToCSV(new Date(), it.rayName, it.clientIP, endpoint, 'Blocked - 429 Too Many Requests', country); logToCSV(new Date(), it.rayName, it.clientIP, url, '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.`); log('warn', `Rate limited (429) while reporting: ${it.clientIP}; URL: ${url}; (Will retry after 5 hours)`);
} else { } else {
logMessage('error', `${err.message} - IP: ${it.clientIP}; Domain: ${it.clientRequestHTTPHost}; Endpoint: ${endpoint}`); log('error', `${err.message} - IP: ${it.clientIP}; Domain: ${it.clientRequestHTTPHost}; URL: ${url}`);
} }
return false; return false;
} }
}; };
@ -184,24 +82,15 @@ const shouldSkipBlockedIP = (ip, blockedIPs) => {
return timeSinceLastBlock < BLOCK_TIME_MS; 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 () => { (async () => {
logMessage('info', 'Starting IP reporting process'); log('info', 'Starting IP reporting process...');
const reportedIPs = readReportedIPs(); const reportedIPs = readReportedIPs();
const skippedRayIds = new Set(reportedIPs.filter(ip => ip.action.startsWith('Skipped')).map(ip => ip.rayid)); 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()])); const blockedIPs = new Map(reportedIPs.filter(ip => ip.action.includes('429')).map(ip => [ip.ip, ip.timestamp.getTime()]));
while (true) { while (true) {
logMessage('info', '===================== New Reporting Cycle ====================='); log('info', '===================== New Reporting Cycle =====================');
const data = await getBlockedIP(); const data = await getBlockedIP();
@ -209,24 +98,22 @@ const readReportedIPs = () => {
const ipBadList = data.data.viewer.zones[0].firewallEventsAdaptive; const ipBadList = data.data.viewer.zones[0].firewallEventsAdaptive;
for (const i of ipBadList) { for (const i of ipBadList) {
const endpoint = `${i.clientRequestHTTPHost}${i.clientRequestPath}`; if (skippedRayIds.has(i.rayName) || shouldSkipBlockedIP(i.clientIP, blockedIPs)) continue;
if (skippedRayIds.has(i.rayName) || shouldSkipBlockedIP(i.clientIP, blockedIPs)) {
continue;
}
if (!exceptedRuleId.has(i.ruleId) && shouldReportDomain(i.clientRequestHTTPHost, reportedIPs)) { if (!exceptedRuleId.has(i.ruleId) && shouldReportDomain(i.clientRequestHTTPHost, reportedIPs)) {
const reported = await reportBadIP(i, skippedRayIds, blockedIPs); const reported = await reportBadIP(i, skippedRayIds, blockedIPs);
if (reported) { if (reported) await new Promise(resolve => setTimeout(resolve, COOLDOWN_MS));
await new Promise(resolve => setTimeout(resolve, COOLDOWN_MS));
}
} else if (!skippedRayIds.has(i.rayName)) { } else if (!skippedRayIds.has(i.rayName)) {
skippedRayIds.add(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)`); const url = `${i.clientRequestHTTPHost}${i.clientRequestPath}`;
logToCSV(new Date(), i.rayName, i.clientIP, url, 'Skipped - Already Reported', i.clientCountryName);
log('info', `Skipping: ${i.clientIP} (domain ${i.clientRequestHTTPHost}); URL: ${url}; (Already reported recently)`);
} }
} }
} }
logMessage('info', '==================== End of Reporting Cycle ===================='); log('info', '==================== End of Reporting Cycle ====================');
await new Promise(resolve => setTimeout(resolve, NODE_ENV === 'production' ? 2 * 60 * 60 * 1000 : 10 * 1000)); await new Promise(resolve => setTimeout(resolve, process.env.NODE_ENV === 'production' ? 2 * 60 * 60 * 1000 : 10 * 1000));
} }
})(); })();

23
scripts/csv.js Normal file
View file

@ -0,0 +1,23 @@
const fs = require('node:fs');
const path = require('node:path');
const CSV_FILE_PATH = path.join(__dirname, '..', 'reported_ips.csv');
if (!fs.existsSync(CSV_FILE_PATH)) fs.writeFileSync(CSV_FILE_PATH, 'Timestamp,RayID,IP,Endpoint,Action,Country\n');
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 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);
};
module.exports = { logToCSV, readReportedIPs };

View file

@ -0,0 +1,23 @@
module.exports = it => {
const fields = [
{ label: 'Action taken', value: it.action?.toUpperCase() },
{ label: 'ASN', value: `${it.clientAsn} (${it.clientASNDescription})` },
{ label: 'HTTP protocol', value: `${it.clientRequestHTTPProtocol} (method ${it.clientRequestHTTPMethodName})` },
{ label: 'Domain', value: it.clientRequestHTTPHost },
{ label: 'Endpoint', value: it.clientRequestPath },
{ label: 'Query', value: it.clientRequestQuery },
{ label: 'Timestamp', value: it.datetime },
{ label: 'Ray ID', value: it.rayName },
{ label: 'Rule ID', value: it.ruleId },
{ label: 'User agent', value: it.userAgent }
];
const reportLines = fields
.filter(field => field.value)
.map(field => `${field.label}: ${field.value}`);
return `IP ${it.clientIP} [${it.clientCountryName}] triggered Cloudflare WAF (${it.source}).
${reportLines.join('\n')}
Report generated by Node-Cloudflare-WAF-AbuseIPDB (https://github.com/sefinek24/Node-Cloudflare-WAF-AbuseIPDB)`;
};

18
scripts/headers.js Normal file
View file

@ -0,0 +1,18 @@
const { name, version, homepage } = require('../package.json');
const userAgent = `Mozilla/5.0 (compatible; ${name}/${version}; +${homepage})`;
const CLOUDFLARE = {
'User-Agent': userAgent,
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.CLOUDFLARE_API_KEY}`,
'X-Auth-Email': process.env.CLOUDFLARE_EMAIL
};
const ABUSEIPDB = {
'User-Agent': userAgent,
'Content-Type': 'application/json',
'Key': process.env.ABUSEIPDB_API_KEY
};
module.exports = { CLOUDFLARE, ABUSEIPDB };

View file

@ -0,0 +1,2 @@
const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.svg', '.webp'];
module.exports = loc => imageExtensions.some(ext => loc.toLowerCase().endsWith(ext));

10
scripts/log.js Normal file
View file

@ -0,0 +1,10 @@
module.exports = (level, message) => {
const logLevels = {
info: '[INFO]',
warn: '[WARN]',
error: '[FAIL]'
};
const timestamp = process.env.NODE_ENV === 'development' ? `${new Date().toISOString()}: ` : '';
console[level](`${logLevels[level]} ${timestamp}${message}`);
};

50
scripts/payload.js Normal file
View file

@ -0,0 +1,50 @@
module.exports = {
query: `query ListFirewallEvents($zoneTag: string, $filter: FirewallEventsAdaptiveFilter_InputObject) {
viewer {
zones(filter: { zoneTag: $zoneTag }) {
firewallEventsAdaptive(
filter: $filter
limit: 200
orderBy: [datetime_DESC]
) {
action
clientASNDescription
clientAsn
clientCountryName
clientIP
clientRequestHTTPHost
clientRequestHTTPMethodName
clientRequestHTTPProtocol
clientRequestPath
clientRequestQuery
datetime
rayName
ruleId
source
userAgent
}
}
}
}`,
variables: {
zoneTag: process.env.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' }
]
}
}
};