Unlink scripts submodule and commit contents as normal directory
This commit is contained in:
1 parent
4f7dea111f
commit
6488b3ea85
48 files changed
+2876
-1
No files matched your search
@@ -0,0 +1,144 @@
|
||||
const fs = require('node:fs/promises');
|
||||
const path = require('node:path');
|
||||
const { existsSync } = require('node:fs');
|
||||
const { parse } = require('csv-parse/sync');
|
||||
const { stringify } = require('csv-stringify/sync');
|
||||
const logger = require('../../logger.js');
|
||||
|
||||
const CSV_FILE = path.join(__dirname, '..', '..', '..', 'tmp', 'reported_ips.csv');
|
||||
const CSV_COLUMNS = ['Timestamp', 'CF RayID', 'IP', 'Country', 'Hostname', 'Endpoint', 'User-Agent', 'Action taken', 'Status', 'Sefinek API'];
|
||||
const MAX_CSV_SIZE = 1024 * 1024;
|
||||
|
||||
const CSV_STRINGIFY_OPTS = {
|
||||
header: true,
|
||||
columns: CSV_COLUMNS,
|
||||
record_delimiter: '\n',
|
||||
quoted: true,
|
||||
quoted_empty: true,
|
||||
quoted_string: true,
|
||||
};
|
||||
|
||||
const CSV_PARSE_OPTS = {
|
||||
columns: true,
|
||||
skip_empty_lines: true,
|
||||
trim: false,
|
||||
skip_records_with_empty_values: false,
|
||||
};
|
||||
|
||||
const ensureCSVExists = async () => {
|
||||
try {
|
||||
if (existsSync(CSV_FILE)) return;
|
||||
await fs.mkdir(path.dirname(CSV_FILE), { recursive: true });
|
||||
await fs.writeFile(CSV_FILE, stringify([], CSV_STRINGIFY_OPTS));
|
||||
logger.success(`Created missing CSV file: ${CSV_FILE}`);
|
||||
} catch (err) {
|
||||
logger.error(`Failed to ensure CSV exists: ${err.stack}`);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const checkCSVSize = async () => {
|
||||
try {
|
||||
if (!existsSync(CSV_FILE)) return;
|
||||
const stats = await fs.stat(CSV_FILE);
|
||||
|
||||
if (stats.size > MAX_CSV_SIZE) {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const backupPath = `${CSV_FILE}.${timestamp}.bkp`;
|
||||
|
||||
await fs.copyFile(CSV_FILE, backupPath);
|
||||
await fs.writeFile(CSV_FILE, stringify([], CSV_STRINGIFY_OPTS));
|
||||
logger.success(`CSV size limit exceeded (${(MAX_CSV_SIZE / (1024 * 1024)).toFixed(2)} MB). Backup saved as "${path.basename(backupPath)}", original file cleared.`);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`Failed to check or clear CSV size: ${err.stack}`);
|
||||
}
|
||||
};
|
||||
|
||||
const logToCSV = async (event, status = 'N/A', sefinekAPI = false) => {
|
||||
try {
|
||||
await ensureCSVExists();
|
||||
await checkCSVSize();
|
||||
|
||||
const {
|
||||
rayName, clientIP, clientCountryName,
|
||||
clientRequestHTTPHost, clientRequestPath,
|
||||
userAgent, action,
|
||||
} = event;
|
||||
|
||||
const row = {
|
||||
'Timestamp': new Date().toISOString(),
|
||||
'CF RayID': rayName,
|
||||
'IP': clientIP,
|
||||
'Country': clientCountryName,
|
||||
'Hostname': clientRequestHTTPHost,
|
||||
'Endpoint': clientRequestPath,
|
||||
'User-Agent': userAgent,
|
||||
'Action taken': action.toUpperCase(),
|
||||
'Status': status,
|
||||
'Sefinek API': String(sefinekAPI),
|
||||
};
|
||||
|
||||
const line = stringify([row], { ...CSV_STRINGIFY_OPTS, header: false });
|
||||
await fs.appendFile(CSV_FILE, line);
|
||||
} catch (err) {
|
||||
logger.error(`Failed to log event to CSV: ${err.stack}`);
|
||||
}
|
||||
};
|
||||
|
||||
const readReportedIPs = async () => {
|
||||
if (!existsSync(CSV_FILE)) return [];
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(CSV_FILE, 'utf-8');
|
||||
const records = parse(content, CSV_PARSE_OPTS);
|
||||
|
||||
return records.map(row => ({
|
||||
timestamp: Date.parse(row['Timestamp']),
|
||||
rayId: row['CF RayID'],
|
||||
ip: row['IP'],
|
||||
country: row['Country'],
|
||||
hostname: row['Hostname'],
|
||||
endpoint: row['Endpoint'],
|
||||
userAgent: row['User-Agent'],
|
||||
action: row['Action taken'],
|
||||
status: row['Status'],
|
||||
sefinekAPI: row['Sefinek API'] === 'true',
|
||||
}));
|
||||
} catch (err) {
|
||||
logger.error(`Failed to parse or map CSV content: ${err.stack}`);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const batchUpdateSefinekAPIInCSV = async (rayIds = []) => {
|
||||
if (!existsSync(CSV_FILE) || !rayIds.length) return;
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(CSV_FILE, 'utf-8');
|
||||
const records = parse(content, CSV_PARSE_OPTS);
|
||||
|
||||
const raySet = new Set(rayIds);
|
||||
let updated = false;
|
||||
|
||||
for (const row of records) {
|
||||
if (raySet.has(row['CF RayID']) && row['Sefinek API'] !== 'true') {
|
||||
row['Sefinek API'] = 'true';
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
const output = stringify(records, CSV_STRINGIFY_OPTS);
|
||||
await fs.writeFile(CSV_FILE, output);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`Batch CSV update failed: ${err.stack}`);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Object.freeze({
|
||||
logToCSV,
|
||||
readReportedIPs,
|
||||
batchUpdateSefinekAPIInCSV,
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
const { MAIN } = require('../../../config.js');
|
||||
|
||||
module.exports = zoneTag => {
|
||||
if (!zoneTag) throw new Error('zoneTag is null or undefined');
|
||||
|
||||
const variables = {
|
||||
zoneTag,
|
||||
filter: {
|
||||
datetime_geq: new Date(Date.now() - MAIN.CLOUDFLARE_TIME_RANGE || 12 * 60 * 60 * 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' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
return { query: `
|
||||
query ListFirewallEvents($zoneTag: string, $filter: FirewallEventsAdaptiveFilter_InputObject) {
|
||||
viewer {
|
||||
zones(filter: { zoneTag: $zoneTag }) {
|
||||
firewallEventsAdaptive(
|
||||
filter: $filter,
|
||||
limit: ${MAIN.CLOUDFLARE_EVENTS_LIMIT || 3000},
|
||||
orderBy: [datetime_DESC]
|
||||
) {
|
||||
action
|
||||
clientASNDescription
|
||||
clientAsn
|
||||
clientCountryName
|
||||
clientIP
|
||||
clientRequestHTTPHost
|
||||
clientRequestHTTPMethodName
|
||||
clientRequestHTTPProtocol
|
||||
clientRequestPath
|
||||
clientRequestQuery
|
||||
datetime
|
||||
rayName
|
||||
ruleId
|
||||
source
|
||||
userAgent
|
||||
}
|
||||
}
|
||||
}
|
||||
}`, variables };
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const PAYLOAD = require('./generateFirewallQuery.js');
|
||||
const { axiosCloudflare } = require('../axios.js');
|
||||
const logger = require('../../logger.js');
|
||||
const { MAIN } = require('../../../config.js');
|
||||
|
||||
const ensureFile = filePath => {
|
||||
if (fs.existsSync(filePath)) return;
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, '');
|
||||
};
|
||||
|
||||
const fetchCloudflareEvents = async () => {
|
||||
const rawZoneIds = MAIN.CLOUDFLARE_ZONE_IDS || MAIN.CLOUDFLARE_ZONE_ID;
|
||||
const zoneIds = Array.isArray(rawZoneIds) ? rawZoneIds : [rawZoneIds];
|
||||
const allEvents = [];
|
||||
|
||||
for (const zoneId of zoneIds) {
|
||||
try {
|
||||
const { data, status } = await axiosCloudflare.post('/graphql', PAYLOAD(zoneId));
|
||||
|
||||
const events = data?.data?.viewer?.zones?.[0]?.firewallEventsAdaptive;
|
||||
if (!events) throw new Error(`Failed to retrieve data from Cloudflare (status ${status}): ${JSON.stringify(data?.errors)}`);
|
||||
|
||||
const l7ddosEvents = events.filter(e => e.source === 'l7ddos');
|
||||
logger.success(`Zone ${zoneId}: ${events.length} events fetched (${l7ddosEvents.length} L7 DDoS)`);
|
||||
allEvents.push(...l7ddosEvents);
|
||||
} catch (err) {
|
||||
logger.error(err.response?.data ? `${err.response.status} HTTP ERROR for zone ${zoneId}: ${JSON.stringify(err.response.data, null, 2)}` : `Unknown error for zone ${zoneId}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return allEvents;
|
||||
};
|
||||
|
||||
const saveToCSV = (events, filePath = 'report.csv') => {
|
||||
const map = new Map();
|
||||
for (const { clientIP, datetime } of events) {
|
||||
if (!map.has(clientIP)) map.set(clientIP, `${clientIP},4,${new Date(datetime).toISOString()},Detected L7 DDoS via Cloudflare`);
|
||||
}
|
||||
|
||||
ensureFile(filePath);
|
||||
if (!fs.statSync(filePath).size) fs.appendFileSync(filePath, 'IP,Categories,ReportDate,Comment\n');
|
||||
|
||||
fs.appendFileSync(filePath, [...map.values()].join('\n') + '\n');
|
||||
};
|
||||
|
||||
const saveToTXT = (events, filePath = 'report.txt') => {
|
||||
const uniqueIPs = new Set(events.map(e => e.clientIP));
|
||||
ensureFile(filePath);
|
||||
fs.appendFileSync(filePath, [...uniqueIPs].join('\n') + '\n');
|
||||
};
|
||||
|
||||
(async () => {
|
||||
const events = await fetchCloudflareEvents();
|
||||
if (events.length) {
|
||||
saveToCSV(events);
|
||||
saveToTXT(events);
|
||||
logger.success(`Saved ${events.length} L7 DDoS events to CSV and TXT`);
|
||||
} else {
|
||||
logger.info('No L7 DDoS events found');
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,22 @@
|
||||
const { axiosGeneric } = require('../axios.js');
|
||||
const logger = require('../../logger.js');
|
||||
|
||||
module.exports = async () => {
|
||||
try {
|
||||
const res = await axiosGeneric.get('https://api.sefinek.net/api/v2/filter-rules');
|
||||
const data = res.data;
|
||||
if (!data.success || typeof data !== 'object') {
|
||||
throw new Error(`Sefinek API error: Invalid response (success=${data.success})`);
|
||||
}
|
||||
|
||||
const { userAgents, domains, endpoints, imgExtensions, lastUpdate } = data;
|
||||
if (!Array.isArray(userAgents) || !Array.isArray(domains) || !Array.isArray(endpoints) || !Array.isArray(imgExtensions)) {
|
||||
throw new Error('Sefinek API error: Response is missing expected arrays');
|
||||
}
|
||||
|
||||
logger.success(`Filter rules loaded from api.sefinek.net, last update: ${lastUpdate}`);
|
||||
return { userAgents, domains, endpoints, imgExtensions, lastUpdate };
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to fetch filter rules: ${err.message}`, { cause: err });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
const FormData = require('form-data');
|
||||
const logger = require('../../logger.js');
|
||||
const { readReportedIPs, batchUpdateSefinekAPIInCSV } = require('./csv.js');
|
||||
const { axiosSefinek } = require('../axios.js');
|
||||
const { getServerIPs } = require('../ipFetcher.js');
|
||||
|
||||
module.exports = async () => {
|
||||
const serverIPs = getServerIPs();
|
||||
const reportedIPs = (await readReportedIPs() || []).filter(x =>
|
||||
['REPORTED', 'READY_FOR_BULK_REPORT', 'RL_BULK_REPORT'].includes(x.status) &&
|
||||
!serverIPs.includes(x.ip) &&
|
||||
!x.sefinekAPI
|
||||
);
|
||||
if (!reportedIPs.length) return logger.info('Sefinek API: No data to report');
|
||||
|
||||
const seenIPs = new Set();
|
||||
const uniqueLogs = reportedIPs.filter(ip => {
|
||||
if (seenIPs.has(ip.ip)) return false;
|
||||
seenIPs.add(ip.ip);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!uniqueLogs.length) return logger.info(`Sefinek API: No unique IPs to send (reportedIPs = ${reportedIPs.length}; uniqueLogs = ${uniqueLogs.length})`);
|
||||
|
||||
try {
|
||||
const payload = uniqueLogs.map(ip => ({
|
||||
rayId: ip.rayId,
|
||||
ip: ip.ip,
|
||||
endpoint: ip.endpoint,
|
||||
userAgent: ip.userAgent,
|
||||
action: ip.action,
|
||||
country: ip.country,
|
||||
timestamp: ip.timestamp,
|
||||
}));
|
||||
|
||||
const base64 = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64');
|
||||
const form = new FormData();
|
||||
form.append('file', base64, {
|
||||
filename: 'reports.dat',
|
||||
contentType: 'application/octet-stream',
|
||||
});
|
||||
|
||||
const res = await axiosSefinek.post('https://api.sefinek.net/api/v2/cloudflare-waf-abuseipdb', form, { headers: { ...form.getHeaders() } });
|
||||
// const res = await axiosSefinek.post('http://127.0.0.1:4010/api/v2/cloudflare-waf-abuseipdb', form, { headers: { ...form.getHeaders() } });
|
||||
if (res.data.success) {
|
||||
logger.success(`Sefinek API (status: ${res.status}): Successfully sent ${uniqueLogs.length} logs`);
|
||||
await batchUpdateSefinekAPIInCSV(uniqueLogs.map(x => x.rayId));
|
||||
} else {
|
||||
logger.warn(`Sefinek API (status: ${res.status}): ${res.data.message || 'Something went wrong'}`);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.response?.data?.message?.includes('No valid or unique')) return;
|
||||
const msg = err.response?.data?.message ?? err.message;
|
||||
logger.error(`Sefinek API (status: ${err.response?.status ?? 'unknown'}): Failed to send logs! Message: ${typeof msg === 'object' ? JSON.stringify(msg) : msg}`, { ping: true });
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user