From aab54d4108cd6f308ab289ef05cec531dc82fa05 Mon Sep 17 00:00:00 2001 From: benya Date: Sun, 8 Feb 2026 02:12:54 +0300 Subject: [PATCH] Add SSL expiry alerts --- CONFIG.en.md | 4 +++ CONFIG.md | 4 +++ config.example.yaml | 9 +++++++ main.py | 3 +++ services/ssl_alerts.py | 57 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 77 insertions(+) create mode 100644 services/ssl_alerts.py diff --git a/CONFIG.en.md b/CONFIG.en.md index 3ae3735..df9d7a1 100644 --- a/CONFIG.en.md +++ b/CONFIG.en.md @@ -58,6 +58,10 @@ Used for SSL certificate status. - `secret` (string): Login password. - `token` (string): Optional static token (not recommended if it expires). - `verify_tls` (bool): Set to `false` for self-signed TLS. +- `alerts.enabled` (bool): Enable expiry notifications. +- `alerts.days` (list): Thresholds in days (e.g. 30/14/7/1). +- `alerts.cooldown_sec` (int): Cooldown between identical alerts. +- `alerts.interval_sec` (int): Check interval. Token flow: diff --git a/CONFIG.md b/CONFIG.md index 8262f99..3d10fb8 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -58,6 +58,10 @@ - `secret` (string): пароль. - `token` (string): опционально статический токен (не рекомендуется при истечении). - `verify_tls` (bool): `false` для self-signed TLS. +- `alerts.enabled` (bool): включить уведомления по истечению. +- `alerts.days` (list): пороги в днях (например 30/14/7/1). +- `alerts.cooldown_sec` (int): кулдаун между одинаковыми алертами. +- `alerts.interval_sec` (int): интервал проверки. Логика токена: diff --git a/config.example.yaml b/config.example.yaml index 938bbca..b3fe558 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -47,6 +47,15 @@ npmplus: # Optional static token (not recommended if it expires) token: "" verify_tls: true + alerts: + enabled: true + days: + - 30 + - 14 + - 7 + - 1 + cooldown_sec: 86400 + interval_sec: 3600 security: reboot_password: "CHANGE_ME" diff --git a/main.py b/main.py index 335b50c..4224144 100644 --- a/main.py +++ b/main.py @@ -9,6 +9,7 @@ from services.metrics import MetricsStore, start_sampler from services.queue import worker as queue_worker from services.notify import notify from services.audit import AuditMiddleware, audit_start +from services.ssl_alerts import monitor_ssl import state import handlers.menu import handlers.status @@ -42,6 +43,8 @@ async def main(): asyncio.create_task(monitor_resources(cfg, notify, bot, ADMIN_ID)) if cfg.get("alerts", {}).get("smart_enabled", True): asyncio.create_task(monitor_smart(cfg, notify, bot, ADMIN_ID)) + if cfg.get("npmplus", {}).get("alerts", {}).get("enabled", True): + asyncio.create_task(monitor_ssl(cfg, notify, bot, ADMIN_ID)) state.METRICS_STORE = MetricsStore() asyncio.create_task(start_sampler(state.METRICS_STORE, interval=5)) asyncio.create_task(queue_worker()) diff --git a/services/ssl_alerts.py b/services/ssl_alerts.py new file mode 100644 index 0000000..5131dc8 --- /dev/null +++ b/services/ssl_alerts.py @@ -0,0 +1,57 @@ +import asyncio +import time +from datetime import datetime, timezone +from typing import Any + +from services.npmplus import fetch_certificates, _parse_expiry + + +async def monitor_ssl(cfg: dict[str, Any], notify, bot, chat_id: int): + npm_cfg = cfg.get("npmplus", {}) + alert_cfg = npm_cfg.get("alerts", {}) + if not alert_cfg.get("enabled", True): + return + + days_list = alert_cfg.get("days", [30, 14, 7, 1]) + days_list = sorted({int(x) for x in days_list if int(x) >= 0}, reverse=True) + cooldown = int(alert_cfg.get("cooldown_sec", 86400)) + interval = int(alert_cfg.get("interval_sec", 3600)) + + last_sent: dict[str, float] = {} + + while True: + now = datetime.now(timezone.utc) + try: + certs = fetch_certificates(cfg) + except Exception: + await asyncio.sleep(interval) + continue + + for cert in certs: + name = cert.get("nice_name") + if not name: + domains = cert.get("domain_names") or [] + if isinstance(domains, list): + name = ", ".join(domains) + if not name: + name = "unknown" + + expiry = _parse_expiry(cert.get("expires_on")) + if expiry is None: + continue + + days_left = (expiry - now).days + for threshold in days_list: + if days_left <= threshold: + key = f"{name}:{threshold}" + last_time = last_sent.get(key, 0) + if time.time() - last_time >= cooldown: + await notify( + bot, + chat_id, + f"⚠️ SSL `{name}` expires in {days_left}d (threshold {threshold}d)", + ) + last_sent[key] = time.time() + break + + await asyncio.sleep(interval)