package main
import (
"crypto/x509"
"encoding/pem"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"sync"
"time"
)
// Поддерживаемые расширения сертификатов
var ccValidExtensions = map[string]bool{
".crt": true, ".pem": true, ".cer": true, ".cert": true,
".der": true, ".p7b": true, ".p7c": true, ".p7s": true, ".csr": true,
}
type ccCertInfo struct {
Path string
NotAfter time.Time
DaysLeft int
CommonName string
Format string
}
// runCertCheck запускает проверку сертификатов
func runCertCheck() error {
hostname, err := os.Hostname()
if err != nil {
hostname = "unknown"
}
log.Printf("Начинаю проверку сертификатов в %s на хосте: %s", cfgSSLDir, hostname)
certPaths, err := ccFindCerts(cfgSSLDir)
if err != nil {
return fmt.Errorf("ошибка при поиске сертификатов: %v", err)
}
log.Printf("Найдено файлов сертификатов: %d", len(certPaths))
pathsChan := make(chan string, len(certPaths))
resultsChan := make(chan ccCertInfo, len(certPaths))
var wg sync.WaitGroup
for i := 0; i < cfgMaxWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for path := range pathsChan {
info, err := ccParseCert(path)
if err != nil {
log.Printf("Не удалось распарсить %s: %v", path, err)
continue
}
resultsChan <- info
}
}()
}
for _, p := range certPaths {
pathsChan <- p
}
close(pathsChan)
go func() {
wg.Wait()
close(resultsChan)
}()
validCerts := 0
for cert := range resultsChan {
validCerts++
ccCheckAndAlert(cert, hostname)
}
log.Printf("Успешно обработано сертификатов: %d", validCerts)
log.Println("Проверка завершена")
return nil
}
func ccFindCerts(root string) ([]string, error) {
var paths []string
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
log.Printf("Ошибка доступа к %s: %v", path, err)
return nil
}
if info.IsDir() {
return nil
}
ext := strings.ToLower(filepath.Ext(path))
if ccValidExtensions[ext] {
paths = append(paths, path)
}
return nil
})
return paths, err
}
func ccParseCert(path string) (ccCertInfo, error) {
data, err := os.ReadFile(path)
if err != nil {
return ccCertInfo{}, err
}
var cert *x509.Certificate
format := "Unknown"
block, _ := pem.Decode(data)
if block != nil {
format = "PEM"
switch block.Type {
case "CERTIFICATE":
cert, err = x509.ParseCertificate(block.Bytes)
case "CERTIFICATE REQUEST":
return ccCertInfo{}, fmt.Errorf("это CSR файл, а не сертификат")
default:
return ccCertInfo{}, fmt.Errorf("неподдерживаемый тип PEM блока: %s", block.Type)
}
} else {
format = "DER"
cert, err = x509.ParseCertificate(data)
}
if err != nil {
return ccCertInfo{}, err
}
daysLeft := int(time.Until(cert.NotAfter).Hours() / 24)
return ccCertInfo{
Path: path, NotAfter: cert.NotAfter, DaysLeft: daysLeft,
CommonName: cert.Subject.CommonName, Format: format,
}, nil
}
func ccCheckAndAlert(cert ccCertInfo, hostname string) {
log.Printf("Сертификат: %s | Format: %s | CN: %s | Истекает: %s | Дней осталось: %d",
cert.Path, cert.Format, cert.CommonName, cert.NotAfter.Format("2006-01-02"), cert.DaysLeft)
var message, emoji string
switch {
case cert.DaysLeft < 0:
emoji = "💀💀💀"
message = fmt.Sprintf(
"%s СЕРТИФИКАТ УЖЕ ИСТЁК! %s\n\n"+
"🖥 Хост: %s\n📄 Путь: %s\n🔐 CN: %s\n"+
"📅 Истёк: %s\n⏱ Просрочен на: %d дней\n📦 Формат: %s\n\n"+
"🔥 КРИТИЧЕСКАЯ СИТУАЦИЯ! СЕРВИС МОЖЕТ НЕ РАБОТАТЬ!",
emoji, emoji, hostname, cert.Path, cert.CommonName,
cert.NotAfter.Format("2006-01-02 15:04:05"), -cert.DaysLeft, cert.Format)
case cert.DaysLeft <= cfgCertCriticalDays:
emoji = "🚨🚨🚨"
message = fmt.Sprintf(
"%s КРИТИЧНО! НЕМЕДЛЕННО ОБНОВИТЕ СЕРТИФИКАТ! %s\n\n"+
"🖥 Хост: %s\n📄 Путь: %s\n🔐 CN: %s\n"+
"📅 Истекает: %s\n⏱ Осталось дней: %d\n📦 Формат: %s\n\n"+
"⚠️ ТРЕБУЕТСЯ НЕМЕДЛЕННОЕ ДЕЙСТВИЕ!",
emoji, emoji, hostname, cert.Path, cert.CommonName,
cert.NotAfter.Format("2006-01-02 15:04:05"), cert.DaysLeft, cert.Format)
case cert.DaysLeft <= cfgCertWarningDays:
emoji = "⚠️⚠️"
message = fmt.Sprintf(
"%s ВНИМАНИЕ! Срочно требуется обновление сертификата! %s\n\n"+
"🖥 Хост: %s\n📄 Путь: %s\n🔐 CN: %s\n"+
"📅 Истекает: %s\n⏱ Осталось дней: %d\n📦 Формат: %s\n\n"+
"Запланируйте обновление в ближайшее время!",
emoji, emoji, hostname, cert.Path, cert.CommonName,
cert.NotAfter.Format("2006-01-02 15:04:05"), cert.DaysLeft, cert.Format)
case cert.DaysLeft <= cfgCertInfoDays:
emoji = "ℹ️"
message = fmt.Sprintf(
"%s Информация: сертификат скоро истечёт\n\n"+
"🖥 Хост: %s\n📄 Путь: %s\n🔐 CN: %s\n"+
"📅 Истекает: %s\n⏱ Осталось дней: %d\n📦 Формат: %s\n\n"+
"Рекомендуется запланировать обновление.",
emoji, hostname, cert.Path, cert.CommonName,
cert.NotAfter.Format("2006-01-02 15:04:05"), cert.DaysLeft, cert.Format)
default:
return // всё ок, алерт не нужен
}
if err := telegramSendHTML(message); err != nil {
log.Printf("Ошибка отправки в Telegram: %v", err)
} else {
log.Println("✅ Алерт успешно отправлен в Telegram")
}
if err := emailSendHTML("Auspex: Срок действия сертификата", message); err != nil {
log.Printf("Ошибка отправки Email: %v", err)
} else {
log.Println("✅ Алерт успешно отправлен на Email")
}
}