import db from '../../config/database'; import { logger } from '../../utils/logger'; import { LogMaskingService } from '../security/LogMaskingService'; import { SemanticLogService } from '../telemetry/SemanticLogService'; import https from 'https'; /** * [BIZ_INF_106] 租户域名证书过期自动预警 (SSL Watch) * @description 核心逻辑:定时扫描租户配置的域名,获取其 SSL 证书到期时间。 * 当到期时间小于 15 天时,自动生成预警并归档至语义日志中心。 * 通过 [LogMaskingService] 屏蔽域名 PII。 */ export class CertsMonitorService { private static readonly ALERT_THRESHOLD_DAYS = 15; // 预警阈值:15天 private static readonly CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 检查间隔:24小时 /** * 初始化定时任务 */ static async init() { this.runCheck(); setInterval(() => this.runCheck(), this.CHECK_INTERVAL_MS); logger.info(`[SSLWatch] Certificate monitor initialized (Alert threshold: ${this.ALERT_THRESHOLD_DAYS} days)`); } /** * 执行证书扫描 */ private static async runCheck() { try { // 1. 获取所有活跃租户的自定义域名 (模拟从 cf_tenants 表中获取) const tenants = await db('cf_tenants').select('id', 'custom_domain').whereNotNull('custom_domain'); for (const tenant of tenants) { if (!tenant.custom_domain) continue; const expirationDate = await this.getCertificateExpirationDate(tenant.custom_domain); if (!expirationDate) continue; const daysRemaining = Math.ceil((expirationDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)); if (daysRemaining <= this.ALERT_THRESHOLD_DAYS) { await this.reportExpiration(tenant.id, tenant.custom_domain, daysRemaining, expirationDate); } } } catch (err: any) { logger.error(`[SSLWatch] Scan failed: ${err.message}`); } } /** * 获取证书过期日期 (通过 HTTPS 请求获取证书) */ private static async getCertificateExpirationDate(domain: string): Promise { return new Promise((resolve) => { const options = { hostname: domain, port: 443, method: 'GET', rejectUnauthorized: false, // 允许自签名或过期证书以便检测 }; const req = https.request(options, (res) => { const cert = (res.socket as any).getPeerCertificate(); if (cert && cert.valid_to) { resolve(new Date(cert.valid_to)); } else { resolve(null); } }); req.on('error', () => resolve(null)); req.end(); }); } /** * 上报预警信息 */ private static async reportExpiration(tenantId: string, domain: string, daysRemaining: number, expirationDate: Date) { const maskedDomain = LogMaskingService.maskData(domain); const message = `🚨 SSL Certificate Expiration Warning: Domain \`${maskedDomain}\` (Tenant: ${tenantId}) will expire in ${daysRemaining} days (on ${expirationDate.toISOString()}). Please renew the certificate immediately.`; await SemanticLogService.logSemantic(message, 'ERROR', 'SECURITY_MONITOR'); logger.error(`[SSLWatch] Certificate for domain ${maskedDomain} is expiring soon.`); } }