Files
makemd/server/src/core/security/CertsMonitorService.ts
wurenzhi 136c2fa579 feat: 初始化项目结构并添加核心功能模块
- 新增文档模板和导航结构
- 实现服务器基础API路由和控制器
- 添加扩展插件配置和前端框架
- 引入多租户和权限管理模块
- 集成日志和数据库配置
- 添加核心业务模型和类型定义
2026-03-17 22:07:19 +08:00

88 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<Date | null> {
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.`);
}
}