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") } }