Unlink scripts submodule and commit contents as normal directory

This commit is contained in:
Kyle Speight committed 2026-06-14 03:42:18 -07:00
1 parent 4f7dea111f
commit 6488b3ea85
48 files changed
+2876 -1

No files matched your search

+144
View File
@@ -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 });
}
};