auspex/certcheck.go
2026-02-24 10:54:38 +03:00

196 lines
5.9 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

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"+
"🖥 Хост: <b>%s</b>\n📄 Путь: <code>%s</code>\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"+
"🖥 Хост: <b>%s</b>\n📄 Путь: <code>%s</code>\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"+
"🖥 Хост: <b>%s</b>\n📄 Путь: <code>%s</code>\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"+
"🖥 Хост: <b>%s</b>\n📄 Путь: <code>%s</code>\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")
}
}