security audit

This commit is contained in:
esef committed 2026-07-13 11:01:42 +03:00
1 parent 19e2db7fd4
commit f16d848e42
7 files changed
+115 -116

No files matched your search

+30 -6
View File
@@ -1,29 +1,53 @@
"""firebase_website — Bot → Firestore sync for the esefos.netlify.app dashboard.
"""firebase_website — Bot → Firestore sync using the Admin SDK.
Add integrations here as they grow:
sysinfo_sync — CPU, RAM, disk, uptime pushed every 10 s
(future) — gacha stats, now-playing, radio listeners, …
Requires GOOGLE_APPLICATION_CREDENTIALS env var pointing to a service-account JSON.
Usage from main.py:
await bot.load_extension("firebase_website")
Provides:
get_db() → google.cloud.firestore.AsyncClient (shared, lazy-init)
"""
######################################################################
import logging
import os
from discord.ext import commands
from google.cloud.firestore import AsyncClient
from .sysinfo_sync import SysInfoSync
log = logging.getLogger(__name__)
_db: AsyncClient | None = None
_sync: SysInfoSync | None = None
def get_db() -> AsyncClient:
"""Return the shared Async Firestore client, creating it on first call."""
global _db
if _db is None:
creds_path = os.getenv("GOOGLE_APPLICATION_CREDENTIALS")
if creds_path is None:
raise RuntimeError(
"GOOGLE_APPLICATION_CREDENTIALS environment variable is not set. "
"Point it to your service-account JSON file."
)
_db = AsyncClient()
log.info("Firestore Admin SDK initialised (project %s)", _db.project)
return _db
async def setup(bot: commands.Bot) -> None:
"""Called by bot.load_extension(). Starts the sysinfo sync loop."""
"""Start the sysinfo sync loop."""
global _sync
get_db() # early init — fails fast if credentials are missing
_sync = SysInfoSync()
_sync.start()
async def teardown(bot: commands.Bot) -> None:
"""Called on bot unload / shutdown. Stops all sync tasks."""
"""Stop the sysinfo sync loop."""
global _sync
if _sync is not None:
_sync.stop()
-11
View File
@@ -1,18 +1,7 @@
"""Firebase project config — shared by all website integrations."""
######################################################################
# Your Firebase project (same one the website uses in comments.js)
PROJECT_ID = "esef-514bf"
API_KEY = "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0"
# Firestore REST API base URL
FIRESTORE_BASE = (
f"https://firestore.googleapis.com/v1/projects/{PROJECT_ID}"
f"/databases/(default)/documents"
)
# Document paths
DOC_SYSINFO = f"{FIRESTORE_BASE}/sysinfo/server"
# How often system stats are pushed (seconds)
SYSINFO_INTERVAL = 10
+35 -50
View File
@@ -1,14 +1,14 @@
"""Push system stats (CPU, RAM, disk, uptime) to Firestore on a loop."""
"""Push system stats (CPU, RAM, disk, uptime) to Firestore via Admin SDK."""
######################################################################
import asyncio
import logging
import re
import time
import aiohttp
import psutil
from .config import DOC_SYSINFO, SYSINFO_INTERVAL
from . import get_db
from .config import SYSINFO_INTERVAL
log = logging.getLogger(__name__)
@@ -27,19 +27,16 @@ class SysInfoSync:
def __init__(self) -> None:
self._task: asyncio.Task | None = None
self._tick = 0
# ── public API ──────────────────────────────────────────────────────────
def start(self) -> None:
"""Launch the background sync loop."""
if self._task is not None:
return
self._task = asyncio.create_task(self._run())
log.info("SysInfo → Firestore sync started (every %ds)", SYSINFO_INTERVAL)
def stop(self) -> None:
"""Cancel the background sync loop."""
if self._task is None:
return
self._task.cancel()
@@ -49,69 +46,57 @@ class SysInfoSync:
# ── internals ───────────────────────────────────────────────────────────
async def _run(self) -> None:
"""Main loop — collect stats, push, sleep, repeat."""
async with aiohttp.ClientSession() as session:
while True:
try:
await self._push(session)
except asyncio.CancelledError:
return
except Exception:
log.debug("SysInfo Firestore push failed", exc_info=True)
await asyncio.sleep(SYSINFO_INTERVAL)
while True:
try:
await self._push()
except asyncio.CancelledError:
return
except Exception:
log.debug("SysInfo Firestore push failed", exc_info=True)
await asyncio.sleep(SYSINFO_INTERVAL)
async def _push(self, session: aiohttp.ClientSession) -> None:
"""Collect current stats and PATCH the Firestore document."""
async def _push(self) -> None:
cpu = psutil.cpu_percent(interval=1)
mem = psutil.virtual_memory()
disk = psutil.disk_usage("/")
uptime_seconds = int(time.time() - psutil.boot_time())
# uptime → "3d 12h 5m" or "5h 23m"
days, r = divmod(uptime_seconds, 86400)
hours, r = divmod(r, 3600)
minutes, _ = divmod(r, 60)
if days > 0:
uptime_str = f"{days}d {hours}h {minutes}m"
else:
uptime_str = f"{hours}h {minutes}m"
uptime_str = f"{days}d {hours}h {minutes}m" if days > 0 else f"{hours}h {minutes}m"
load1, load5, load15 = psutil.getloadavg()
net = psutil.net_io_counters()
swap = psutil.swap_memory()
payload = {
"fields": {
"online": {"booleanValue": True},
"cpu": {"doubleValue": round(cpu, 1)},
"ram": {"doubleValue": round(mem.percent, 1)},
"ramUsed": {"stringValue": f"{mem.used // (1024 ** 2)}MB"},
"ramTotal": {"stringValue": f"{mem.total // (1024 ** 2)}MB"},
"disk": {"doubleValue": round(disk.percent, 1)},
"diskUsed": {"stringValue": f"{disk.used // (1024 ** 3)}GB"},
"diskTotal": {"stringValue": f"{disk.total // (1024 ** 3)}GB"},
"swap": {"doubleValue": round(swap.percent, 1)},
"swapUsed": {"stringValue": f"{swap.used // (1024 ** 2)}MB"},
"swapTotal": {"stringValue": f"{swap.total // (1024 ** 2)}MB"},
"uptime": {"stringValue": uptime_str},
"load1": {"doubleValue": round(load1, 1)},
"load5": {"doubleValue": round(load5, 1)},
"load15": {"doubleValue": round(load15, 1)},
"netSent": {"stringValue": _fmt_bytes(net.bytes_sent)},
"netRecv": {"stringValue": _fmt_bytes(net.bytes_recv)},
"processes": {"integerValue": len(psutil.pids())},
"fetch": {"stringValue": await self._get_fastfetch()},
}
"online": True,
"cpu": round(cpu, 1),
"ram": round(mem.percent, 1),
"ramUsed": f"{mem.used // (1024 ** 2)}MB",
"ramTotal": f"{mem.total // (1024 ** 2)}MB",
"disk": round(disk.percent, 1),
"diskUsed": f"{disk.used // (1024 ** 3)}GB",
"diskTotal": f"{disk.total // (1024 ** 3)}GB",
"swap": round(swap.percent, 1),
"swapUsed": f"{swap.used // (1024 ** 2)}MB",
"swapTotal": f"{swap.total // (1024 ** 2)}MB",
"uptime": uptime_str,
"load1": round(load1, 1),
"load5": round(load5, 1),
"load15": round(load15, 1),
"netSent": _fmt_bytes(net.bytes_sent),
"netRecv": _fmt_bytes(net.bytes_recv),
"processes": len(psutil.pids()),
"fetch": await self._get_fastfetch(),
}
async with session.patch(DOC_SYSINFO, json=payload) as resp:
if resp.status not in (200, 201):
body = await resp.text()
log.debug("Firestore PATCH %d: %s", resp.status, body[:200])
db = get_db()
await db.collection("sysinfo").document("server").set(payload, merge=True)
@staticmethod
async def _get_fastfetch() -> str:
"""Run fastfetch --pipe --logo none, strip ANSI, return first 15 lines."""
try:
proc = await asyncio.create_subprocess_exec(
"fastfetch", "--pipe", "--logo", "none",
@@ -120,7 +105,7 @@ class SysInfoSync:
)
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=5)
clean = re.sub(r"\x1b\[[0-9;]*[a-zA-Z]|\[[0-9]+[A-Z]", "", stdout.decode())
lines = [l for l in clean.strip().splitlines() if l.strip()]
lines = [line for line in clean.strip().splitlines() if line.strip()]
return "\n".join(lines[:15])
except Exception:
return ""