diff --git a/autoRun/location1AbuseMonitor.js b/autoRun/location1AbuseMonitor.js new file mode 100644 index 0000000..eda08e0 --- /dev/null +++ b/autoRun/location1AbuseMonitor.js @@ -0,0 +1,239 @@ +const axios = require('axios'); +const cron = require('node-cron'); +const { EmbedBuilder, Colors } = require('discord.js'); +const { getVoiceConnection, createAudioPlayer, createAudioResource, AudioPlayerStatus } = require('@discordjs/voice'); +const config = require('../config.json'); + +const LOCATION_ID = 1; +const LOG_CHANNEL_ID = '1387858714027098163'; +const ALERT_SOUND = 'https://actions.google.com/sounds/v1/alarms/beep_short.ogg'; +const SUSPICIOUS_KEYWORDS = ['xmrig', 'miner', 'botnet', 'stress', 'ddos', 'udp', 'attack', 'crypto']; + +const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +const bytesToMB = (bytes = 0) => Math.round(bytes / 1024 / 1024); + +const playAlertSound = (client) => { + try { + const connection = getVoiceConnection(config.settings.guildID); + if (!connection) return; + + const player = createAudioPlayer(); + const resource = createAudioResource(ALERT_SOUND); + + player.on('error', (error) => console.error('[Voice Alert] Playback error:', error.message)); + connection.subscribe(player); + player.play(resource); + + player.once(AudioPlayerStatus.Idle, () => player.stop()); + } catch (error) { + console.error('[Voice Alert] Failed to play alert sound:', error.message); + } +}; + +const fetchServersInLocation = async () => { + const servers = []; + + const nodesResponse = await axios({ + url: `${config.pterodactyl.host}/api/application/nodes`, + method: 'GET', + params: { per_page: 100 }, + headers: { + Authorization: `Bearer ${config.pterodactyl.adminApiKey}`, + 'Content-Type': 'application/json', + Accept: 'Application/vnd.pterodactyl.v1+json', + }, + }); + + const locationNodes = nodesResponse.data?.data?.filter((node) => node?.attributes?.location_id === LOCATION_ID) || []; + + for (const node of locationNodes) { + await wait(400); // respect panel rate limits + const detailedNode = await axios({ + url: `${config.pterodactyl.host}/api/application/nodes/${node.attributes.id}`, + method: 'GET', + params: { include: 'servers' }, + headers: { + Authorization: `Bearer ${config.pterodactyl.adminApiKey}`, + 'Content-Type': 'application/json', + Accept: 'Application/vnd.pterodactyl.v1+json', + }, + }); + + const serverData = detailedNode.data?.attributes?.relationships?.servers?.data || []; + serverData.forEach((srv) => { + if (srv?.attributes?.identifier) { + servers.push({ + identifier: srv.attributes.identifier, + name: srv.attributes.name, + limits: srv.attributes.limits || {}, + }); + } + }); + } + + return servers; +}; + +const checkSuspiciousFiles = async (serverIdentifier) => { + const suspiciousFindings = []; + + try { + const listResponse = await axios({ + url: `${config.pterodactyl.host}/api/client/servers/${serverIdentifier}/files/list`, + method: 'GET', + params: { directory: '/' }, + headers: { + Authorization: `Bearer ${config.pterodactyl.clientAPI}`, + 'Content-Type': 'application/json', + Accept: 'Application/vnd.pterodactyl.v1+json', + }, + }); + + const files = listResponse.data?.data || []; + const flaggedFiles = files.filter((file) => file?.attributes?.is_file && SUSPICIOUS_KEYWORDS.some((keyword) => file.attributes.name.toLowerCase().includes(keyword))); + + for (const file of flaggedFiles.slice(0, 3)) { // keep content checks light + await wait(300); + try { + const contentResponse = await axios({ + url: `${config.pterodactyl.host}/api/client/servers/${serverIdentifier}/files/contents`, + method: 'GET', + params: { file: `/${file.attributes.name}` }, + headers: { + Authorization: `Bearer ${config.pterodactyl.clientAPI}`, + 'Content-Type': 'application/json', + Accept: 'Application/vnd.pterodactyl.v1+json', + }, + timeout: 5000, + }); + + const snippet = (contentResponse.data || '').toString().slice(0, 200).toLowerCase(); + const contentKeyword = SUSPICIOUS_KEYWORDS.find((keyword) => snippet.includes(keyword)); + if (contentKeyword) { + suspiciousFindings.push({ + name: file.attributes.name, + reason: `Keyword match in content: ${contentKeyword}`, + }); + } else { + suspiciousFindings.push({ + name: file.attributes.name, + reason: 'Suspicious filename', + }); + } + } catch (error) { + suspiciousFindings.push({ + name: file.attributes.name, + reason: `Could not read content (${error?.response?.status || error.message})`, + }); + } + } + } catch (error) { + console.error(`[Abuse Monitor] Failed to list files for ${serverIdentifier}:`, error.message); + } + + return suspiciousFindings; +}; + +const checkServerUsage = async (server) => { + const issues = []; + + try { + const resourceResponse = await axios({ + url: `${config.pterodactyl.host}/api/client/servers/${server.identifier}/resources`, + method: 'GET', + headers: { + Authorization: `Bearer ${config.pterodactyl.clientAPI}`, + 'Content-Type': 'application/json', + Accept: 'Application/vnd.pterodactyl.v1+json', + }, + }); + + const resources = resourceResponse.data?.attributes?.resources || {}; + const limits = server.limits || {}; + + if (resources.current_state === 'offline') { + return issues; + } + + if (limits.memory && resources.memory_bytes > limits.memory * 1024 * 1024) { + issues.push(`Memory over limit: ${bytesToMB(resources.memory_bytes)}MB / ${limits.memory}MB`); + } + + if (limits.disk && resources.disk_bytes > limits.disk * 1024 * 1024) { + issues.push(`Disk over limit: ${bytesToMB(resources.disk_bytes)}MB / ${limits.disk}MB`); + } + + if (limits.cpu && limits.cpu > 0 && resources.cpu_absolute > limits.cpu) { + issues.push(`CPU over limit: ${resources.cpu_absolute.toFixed(1)}% / ${limits.cpu}%`); + } + } catch (error) { + issues.push(`Could not fetch resources (${error?.response?.status || error.message})`); + } + + return issues; +}; + +module.exports = async (client) => { + if (!config.settings.serverchecker) return; + + console.log('[AutoRun] Location 1 abuse monitor scheduled (every hour).'); + + cron.schedule('0 * * * *', async () => { + const logChannel = client.channels.cache.get(LOG_CHANNEL_ID); + const flaggedServers = []; + + try { + const servers = await fetchServersInLocation(); + + for (const server of servers) { + await wait(500); + const issues = await checkServerUsage(server); + + if (issues.length === 0) continue; + + const suspiciousFiles = await checkSuspiciousFiles(server.identifier); + + if (issues.length > 0 || suspiciousFiles.length > 0) { + flaggedServers.push({ ...server, issues, suspiciousFiles }); + } + } + } catch (error) { + console.error('[Abuse Monitor] Failed to complete scan:', error.message); + } + + if (!flaggedServers.length) return; + + const embed = new EmbedBuilder() + .setTitle('Location 1 Abuse Scan') + .setColor(Colors.Red) + .setDescription(`Identified potential resource abuse on ${flaggedServers.length} server(s).`) + .setTimestamp(); + + flaggedServers.slice(0, 10).forEach((server) => { + const fileNotes = server.suspiciousFiles?.map((file) => `• ${file.name} — ${file.reason}`).join('\n'); + const issueLines = server.issues.map((issue) => `• ${issue}`).join('\n'); + + const details = [issueLines, fileNotes].filter(Boolean).join('\n'); + embed.addFields({ + name: server.name || server.identifier, + value: details || 'Issues detected but no details available.', + }); + }); + + if (flaggedServers.length > 10) { + embed.addFields({ + name: 'Additional servers', + value: `${flaggedServers.length - 10} more server(s) flagged beyond embed limit.`, + }); + } + + if (logChannel) { + logChannel.send({ embeds: [embed] }).catch((err) => console.error('[Abuse Monitor] Failed to log to channel:', err.message)); + } else { + console.warn(`[Abuse Monitor] Log channel ${LOG_CHANNEL_ID} not found.`); + } + + playAlertSound(client); + }); +}; diff --git a/commands/admin/powerofflocation1.js b/commands/admin/powerofflocation1.js new file mode 100644 index 0000000..1be91f2 --- /dev/null +++ b/commands/admin/powerofflocation1.js @@ -0,0 +1,142 @@ +const Discord = require('discord.js'); +const axios = require('axios'); +const config = require('../../config.json'); + +const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +module.exports = { + name: 'powerofflocation1', + category: 'Owner', + description: 'Power off all servers in location 1.', + aliases: ['shutdownloc1'], + run: async (client, message) => { + if (message.author.id !== config.settings.owner) return; + + const locationId = 1; + const summary = { + totalServers: 0, + stopSent: 0, + killSent: 0, + failed: 0, + }; + + const embed = new Discord.EmbedBuilder() + .setColor(Discord.Colors.Red) + .setTitle('Powering off servers') + .setDescription(`Collecting servers for location **${locationId}**...`) + .setTimestamp(); + + const statusMessage = await message.reply({ embeds: [embed] }); + + try { + const nodeResponse = await axios({ + url: `${config.pterodactyl.host}/api/application/nodes`, + method: 'GET', + params: { per_page: 100 }, + headers: { + Authorization: `Bearer ${config.pterodactyl.adminApiKey}`, + 'Content-Type': 'application/json', + Accept: 'Application/vnd.pterodactyl.v1+json', + }, + }); + + const locationNodes = nodeResponse.data?.data?.filter( + (node) => node?.attributes?.location_id === locationId, + ) || []; + + if (locationNodes.length === 0) { + embed.setDescription(`No nodes found for location **${locationId}**.`).setColor(Discord.Colors.Orange); + return statusMessage.edit({ embeds: [embed] }); + } + + const servers = []; + + for (const node of locationNodes) { + await wait(250); // small delay for rate limiting + const detailedNode = await axios({ + url: `${config.pterodactyl.host}/api/application/nodes/${node.attributes.id}`, + method: 'GET', + params: { include: 'servers' }, + headers: { + Authorization: `Bearer ${config.pterodactyl.adminApiKey}`, + 'Content-Type': 'application/json', + Accept: 'Application/vnd.pterodactyl.v1+json', + }, + }); + + const serverData = detailedNode.data?.attributes?.relationships?.servers?.data || []; + serverData.forEach((srv) => { + if (srv?.attributes?.identifier) { + servers.push({ + identifier: srv.attributes.identifier, + name: srv.attributes.name, + }); + } + }); + } + + if (servers.length === 0) { + embed.setDescription(`No servers found in location **${locationId}**.`).setColor(Discord.Colors.Orange); + return statusMessage.edit({ embeds: [embed] }); + } + + summary.totalServers = servers.length; + embed.setDescription(`Sending power off signals to **${summary.totalServers}** servers...`); + await statusMessage.edit({ embeds: [embed] }); + + for (const server of servers) { + await wait(500); // avoid hitting API rate limits + try { + await axios({ + url: `${config.pterodactyl.host}/api/client/servers/${server.identifier}/power`, + method: 'POST', + data: { signal: 'stop' }, + headers: { + Authorization: `Bearer ${config.pterodactyl.clientAPI}`, + 'Content-Type': 'application/json', + Accept: 'Application/vnd.pterodactyl.v1+json', + }, + }); + summary.stopSent += 1; + } catch (err) { + summary.failed += 1; + } + + await wait(500); + try { + await axios({ + url: `${config.pterodactyl.host}/api/client/servers/${server.identifier}/power`, + method: 'POST', + data: { signal: 'kill' }, + headers: { + Authorization: `Bearer ${config.pterodactyl.clientAPI}`, + 'Content-Type': 'application/json', + Accept: 'Application/vnd.pterodactyl.v1+json', + }, + }); + summary.killSent += 1; + } catch (err) { + summary.failed += 1; + } + } + + embed + .setColor(Discord.Colors.Green) + .setTitle('Power off complete') + .setDescription(`Processed **${summary.totalServers}** servers in location **${locationId}**.`) + .setFields( + { name: 'Stop signals', value: summary.stopSent.toString(), inline: true }, + { name: 'Kill signals', value: summary.killSent.toString(), inline: true }, + { name: 'Failures', value: summary.failed.toString(), inline: true }, + ); + } catch (error) { + embed + .setColor(Discord.Colors.Red) + .setTitle('Power off failed') + .setDescription('An error occurred while powering off servers.') + .addFields({ name: 'Error', value: error?.message || 'Unknown error' }); + } + + return statusMessage.edit({ embeds: [embed] }); + }, +};