Notification (Telegram) for SSL notification thresholds

Ensure Telegram notifications respect warning/expiry thresholds, including manual checks.
This commit is contained in:
Tola Leng
2025-05-16 22:18:35 +08:00
parent 687619ce0d
commit 8ee933563e
4 changed files with 346 additions and 0 deletions
@@ -0,0 +1,10 @@
// Export all SSL notification related functionality
export {
checkAllCertificatesAndNotify,
checkCertificateAndNotify,
testCertificateNotification
} from './sslCheckNotifier';
export { sendSSLNotification } from './sslNotificationSender';
export { shouldRunDailyCheck } from './sslScheduleUtils';
@@ -0,0 +1,168 @@
import { pb } from "@/lib/pocketbase";
import { SSLCertificate } from "../types";
import { checkSSLCertificate } from "../sslCheckerService";
import { determineSSLStatus } from "../sslStatusUtils";
import { sendSSLNotification } from "./sslNotificationSender";
import { toast } from "sonner";
/**
* Checks all SSL certificates and sends notifications for expiring ones
* This should be called once per day
*/
export async function checkAllCertificatesAndNotify(): Promise<void> {
console.log("Starting daily SSL certificates check...");
try {
// Fetch all SSL certificates from database
const response = await pb.collection('ssl_certificates').getList(1, 100, {});
// Properly cast the items as SSLCertificate
const certificates = response.items as unknown as SSLCertificate[];
console.log(`Found ${certificates.length} certificates to check`);
// Check each certificate
for (const cert of certificates) {
await checkCertificateAndNotify(cert);
}
console.log("Daily SSL certificates check completed");
} catch (error) {
console.error("Error during SSL certificates daily check:", error);
}
}
/**
* Checks a specific SSL certificate and sends notification if needed
* This respects the Warning and Expiry Thresholds set on the certificate
*/
export async function checkCertificateAndNotify(certificate: SSLCertificate): Promise<boolean> {
console.log(`Checking certificate for ${certificate.domain}...`);
try {
// Get fresh SSL data
const sslData = await checkSSLCertificate(certificate.domain);
if (!sslData || !sslData.result) {
console.error(`Failed to check SSL for ${certificate.domain}`);
return false;
}
// Extract days left from the check result
const daysLeft = sslData.result.days_left || 0;
// Get threshold values (ensure they are numbers)
const warningThreshold = Number(certificate.warning_threshold) || 30;
const expiryThreshold = Number(certificate.expiry_threshold) || 7;
console.log(`Certificate ${certificate.domain} thresholds: warning=${warningThreshold}, expiry=${expiryThreshold}, days left=${daysLeft}`);
// Update status based on thresholds
const status = determineSSLStatus(daysLeft, warningThreshold, expiryThreshold);
// Check if we should send a notification based on thresholds
let shouldNotify = false;
let isCritical = false;
// Critical notifications - when days left is less than or equal to expiry threshold
if (daysLeft <= expiryThreshold) {
shouldNotify = true;
isCritical = true;
}
// Warning notifications - when days left is less than or equal to warning threshold but greater than expiry threshold
else if (daysLeft <= warningThreshold) {
shouldNotify = true;
isCritical = false;
}
console.log(`${certificate.domain}: ${daysLeft} days left, status: ${status}, should notify: ${shouldNotify}, critical: ${isCritical}`);
// Update certificate data in database
const updateData: Partial<SSLCertificate> = {
days_left: daysLeft,
status: status,
// Other fields from the SSL check that should be updated
issuer_o: sslData.result.issuer_o || certificate.issuer_o,
valid_from: sslData.result.valid_from || certificate.valid_from,
valid_till: sslData.result.valid_till || certificate.valid_till,
validity_days: sslData.result.validity_days || certificate.validity_days,
cert_sans: sslData.result.cert_sans,
cert_alg: sslData.result.cert_alg,
serial_number: sslData.result.cert_sn
};
await pb.collection('ssl_certificates').update(certificate.id, updateData);
// Send notification if needed
if (shouldNotify && certificate.notification_channel) {
console.log(`Sending notification for ${certificate.domain}`);
// Different message based on expiry threshold
const message = isCritical
? `🚨 CRITICAL: SSL Certificate for ${certificate.domain} will expire in ${daysLeft} days!`
: `⚠️ WARNING: SSL Certificate for ${certificate.domain} will expire in ${daysLeft} days.`;
// Send the notification using our specialized SSL notification sender
const notificationSent = await sendSSLNotification(certificate, message, isCritical);
if (notificationSent) {
// Update last_notified timestamp
await pb.collection('ssl_certificates').update(certificate.id, {
last_notified: new Date().toISOString()
});
console.log(`Notification sent for ${certificate.domain}`);
// Show toast for manual checks
toast.success(`Notification sent for ${certificate.domain}`);
return true;
} else {
console.error(`Failed to send notification for ${certificate.domain}`);
// Show error toast for manual checks
toast.error(`Failed to send notification for ${certificate.domain}`);
return false;
}
} else if (shouldNotify && !certificate.notification_channel) {
console.log(`No notification channel set for ${certificate.domain}, skipping notification`);
toast.info(`No notification channel set for ${certificate.domain}, skipping notification`);
} else {
console.log(`No notification needed for ${certificate.domain} (${daysLeft} days left)`);
// For manual checks, inform the user that thresholds weren't met
toast.info(`Certificate for ${certificate.domain} is valid (${daysLeft} days left)`);
}
return true;
} catch (error) {
console.error(`Error checking certificate for ${certificate.domain}:`, error);
toast.error(`Error checking certificate: ${error instanceof Error ? error.message : "Unknown error"}`);
return false;
}
}
/**
* Manual trigger for certificate notification test
* Useful for testing notification channels
*/
export async function testCertificateNotification(certificate: SSLCertificate): Promise<boolean> {
try {
console.log(`Testing notification for ${certificate.domain}...`);
// Create test message
const message = `🧪 TEST: SSL Certificate notification for ${certificate.domain}.`;
// We set isCritical to false for test notifications
const notificationSent = await sendSSLNotification(certificate, message, false);
if (notificationSent) {
console.log(`Test notification sent for ${certificate.domain}`);
toast.success(`Test notification sent for ${certificate.domain}`);
return true;
} else {
console.error(`Failed to send test notification for ${certificate.domain}`);
toast.error(`Failed to send test notification for ${certificate.domain}`);
return false;
}
} catch (error) {
console.error(`Error sending test notification for ${certificate.domain}:`, error);
toast.error(`Error sending test notification: ${error instanceof Error ? error.message : "Unknown error"}`);
return false;
}
}
@@ -0,0 +1,141 @@
import { pb } from "@/lib/pocketbase";
import { SSLCertificate, SSLNotification } from "../types";
import { AlertConfiguration } from "@/services/alertConfigService";
/**
* Dedicated function to send SSL certificate notifications
*/
export async function sendSSLNotification(
certificate: SSLCertificate,
message: string,
isCritical: boolean
): Promise<boolean> {
try {
// Check if notification channel is set
if (!certificate.notification_channel) {
console.log(`No notification channel set for certificate: ${certificate.domain}`);
return false;
}
// Fetch the notification configuration
const alertConfigRecord = await pb.collection('alert_configurations').getOne(certificate.notification_channel);
if (!alertConfigRecord) {
console.error(`Alert configuration not found for ID: ${certificate.notification_channel}`);
return false;
}
if (!alertConfigRecord.enabled) {
console.log(`Alert configuration is disabled for certificate: ${certificate.domain}`);
return false;
}
// Convert PocketBase record to AlertConfiguration
const alertConfig: AlertConfiguration = {
id: alertConfigRecord.id,
collectionId: alertConfigRecord.collectionId,
collectionName: alertConfigRecord.collectionName,
service_id: alertConfigRecord.service_id || "",
notification_type: alertConfigRecord.notification_type,
telegram_chat_id: alertConfigRecord.telegram_chat_id,
discord_webhook_url: alertConfigRecord.discord_webhook_url,
signal_number: alertConfigRecord.signal_number,
notify_name: alertConfigRecord.notify_name,
bot_token: alertConfigRecord.bot_token,
template_id: alertConfigRecord.template_id,
slack_webhook_url: alertConfigRecord.slack_webhook_url,
enabled: alertConfigRecord.enabled,
created: alertConfigRecord.created,
updated: alertConfigRecord.updated
};
// Create an SSL notification object for tracking
const sslNotification: SSLNotification = {
certificateId: certificate.id,
domain: certificate.domain,
message: message,
isCritical: isCritical,
timestamp: new Date().toISOString()
};
// Send the notification based on the notification type
return await sendNotificationByType(alertConfig, certificate, message, isCritical, sslNotification);
} catch (error) {
console.error("Error sending SSL notification:", error);
return false;
}
}
/**
* Send notification based on notification channel type
*/
async function sendNotificationByType(
alertConfig: AlertConfiguration,
certificate: SSLCertificate,
message: string,
isCritical: boolean,
notification: SSLNotification
): Promise<boolean> {
switch (alertConfig.notification_type) {
case 'telegram':
return await sendTelegramNotification(alertConfig, certificate, message, isCritical);
// Add other notification types as they are implemented
// case 'discord':
// return await sendDiscordNotification(alertConfig, certificate, message, isCritical);
// case 'slack':
// return await sendSlackNotification(alertConfig, certificate, message, isCritical);
default:
console.log(`Notification type ${alertConfig.notification_type} not implemented yet for SSL certificates`);
return false;
}
}
/**
* Send notification via Telegram
*/
async function sendTelegramNotification(
alertConfig: AlertConfiguration,
certificate: SSLCertificate,
message: string,
isCritical: boolean
): Promise<boolean> {
if (!alertConfig.bot_token || !alertConfig.telegram_chat_id) {
console.error("Missing Telegram bot token or chat ID");
return false;
}
const telegramUrl = `https://api.telegram.org/bot${alertConfig.bot_token}/sendMessage`;
const severity = isCritical ? "🚨 CRITICAL" : "⚠️ WARNING";
// Enhanced message with more SSL certificate details
const enhancedMessage = `${severity}: SSL Certificate Alert
Domain: ${certificate.domain}
Days Remaining: ${certificate.days_left}
Expiration Date: ${certificate.valid_till}
Status: ${certificate.status.toUpperCase()}
${message}
This is an automated notification from your SSL certificate monitoring system.`;
try {
const response = await fetch(telegramUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: alertConfig.telegram_chat_id,
text: enhancedMessage,
parse_mode: 'Markdown'
})
});
const result = await response.json();
return result.ok === true;
} catch (error) {
console.error("Error sending Telegram notification:", error);
return false;
}
}
@@ -0,0 +1,27 @@
/**
* Check if SSL certificate daily check should run
* This helps implement the once-per-day check logic
*/
export function shouldRunDailyCheck(): boolean {
const lastRunKey = 'ssl_daily_check_last_run';
const lastRun = localStorage.getItem(lastRunKey);
if (!lastRun) {
// First time running, save timestamp and run
localStorage.setItem(lastRunKey, new Date().toISOString());
return true;
}
const lastRunDate = new Date(lastRun).setHours(0, 0, 0, 0);
const today = new Date().setHours(0, 0, 0, 0);
// If last run was not today, run again
if (lastRunDate < today) {
localStorage.setItem(lastRunKey, new Date().toISOString());
return true;
}
// Already ran today
return false;
}