Main commit

This commit is contained in:
Sefinek 2024-08-15 10:31:55 +02:00
commit 857c7cf90d
8 changed files with 536 additions and 0 deletions

6
.env.default Normal file
View file

@ -0,0 +1,6 @@
NODE_ENV=production
CLOUDFLARE_ZONE_ID=
CLOUDFLARE_EMAIL=
CLOUDFLARE_API_KEY=
ABUSEIPDB_API_KEY=

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
.idea
node_modules
.env
reported_ips.csv

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Sefinek
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

33
ecosystem.config.js Normal file
View file

@ -0,0 +1,33 @@
module.exports = {
apps: [{
name: 'waf-abuseipdb',
script: './index.js',
// Configuration options
exec_mode: 'fork',
max_memory_restart: '500M',
// Monitoring changes in files and restarting the application
watch: false,
ignore_watch: ['.git', 'node_modules', 'logs', 'eslint.config.mjs', 'ecosystem.config.js'],
// Logging settings
log_date_format: 'HH:mm:ss.SSS DD.MM.YYYY',
merge_logs: true,
log_file: '/home/sefinek/logs/other/waf-abuseipdb/combined.log',
out_file: '/home/sefinek/logs/other/waf-abuseipdb/out.log',
error_file: '/home/sefinek/logs/other/waf-abuseipdb/error.log',
// Application restart policy
wait_ready: true,
autorestart: true,
max_restarts: 4,
restart_delay: 4000,
min_uptime: 3000,
// Environment variables
env: {
NODE_ENV: 'production'
}
}]
};

57
eslint.config.mjs Normal file
View file

@ -0,0 +1,57 @@
import js from '@eslint/js';
import globals from 'globals';
// noinspection JSUnusedGlobalSymbols
export default [
js.configs.recommended,
{
languageOptions: {
ecmaVersion: 2024,
globals: {
...globals.node,
...globals.es2024,
...globals.browser,
}
},
rules: {
'arrow-spacing': ['warn', { before: true, after: true }],
'comma-dangle': ['error'],
'comma-spacing': 'error',
'comma-style': 'error',
'curly': ['error', 'multi-line', 'consistent'],
'dot-location': ['error', 'property'],
'handle-callback-err': 'off',
'indent': ['warn', 'tab'],
'keyword-spacing': 'warn',
'max-nested-callbacks': ['error', { max: 4 }],
'max-statements-per-line': ['error', { max: 2 }],
'no-console': 'off',
'no-empty': 'warn',
'no-empty-function': 'error',
'no-floating-decimal': 'error',
'no-lonely-if': 'error',
'no-multi-spaces': 'warn',
'no-multiple-empty-lines': ['warn', { max: 4, maxEOF: 1, maxBOF: 0 }],
'no-shadow': ['error', { allow: ['err', 'resolve', 'reject'] }],
'no-trailing-spaces': ['warn'],
'no-unreachable': 'warn',
'no-unused-vars': 'warn',
'no-use-before-define': ['error', { functions: false, classes: true }],
'no-var': 'error',
'object-curly-spacing': ['error', 'always'],
'prefer-const': 'error',
'quotes': ['warn', 'single'],
'semi': ['warn', 'always'],
'sort-vars': 'warn',
'space-before-blocks': 'error',
'space-before-function-paren': ['error', { anonymous: 'never', named: 'never', asyncArrow: 'always' }],
'space-in-parens': 'error',
'space-infix-ops': 'error',
'space-unary-ops': 'error',
'spaced-comment': 'warn',
'wrap-regex': 'error',
'yoda': 'error'
},
ignores: ['node_modules', 'public/js/bootstrap.bundle.min.js']
}
];

232
index.js Normal file
View file

@ -0,0 +1,232 @@
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));
}
})();

155
package-lock.json generated Normal file
View file

@ -0,0 +1,155 @@
{
"name": "node-cf-waf-abuseipdb",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "node-cf-waf-abuseipdb",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"axios": "^1.7.4",
"dotenv": "^16.4.5"
},
"devDependencies": {
"@eslint/js": "^9.9.0",
"globals": "^15.9.0"
}
},
"node_modules/@eslint/js": {
"version": "9.9.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.0.tgz",
"integrity": "sha512-hhetes6ZHP3BlXLxmd8K2SNgkhNSi+UcecbnwWKwpP7kyi/uC75DJ1lOOBO3xrC4jyojtGE3YxKZPHfk4yrgug==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dotenv": {
"version": "16.4.5",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/follow-redirects": {
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/globals": {
"version": "15.9.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz",
"integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
}
}
}

28
package.json Normal file
View file

@ -0,0 +1,28 @@
{
"name": "node-cf-waf-abuseipdb",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"up": "ncu -u && npm install && npm update && npm audit fix"
},
"repository": {
"type": "git",
"url": "git+https://github.com/sefinek24/Node-Cloudflare-WAF-AbuseIPDB.git"
},
"author": "Sefinek <contact@nekosia.cat> (https://sefinek.net)",
"license": "MIT",
"bugs": {
"url": "https://github.com/sefinek24/Node-Cloudflare-WAF-AbuseIPDB/issues"
},
"homepage": "https://github.com/sefinek24/Node-Cloudflare-WAF-AbuseIPDB#readme",
"description": "",
"devDependencies": {
"@eslint/js": "^9.9.0",
"globals": "^15.9.0"
},
"dependencies": {
"axios": "^1.7.4",
"dotenv": "^16.4.5"
}
}