344 lines
11 KiB
Go
344 lines
11 KiB
Go
|
|
package monitoring
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"service-operation/operations"
|
|
"service-operation/pocketbase"
|
|
"service-operation/types"
|
|
)
|
|
|
|
type SSLMonitoringService struct {
|
|
pbClient *pocketbase.PocketBaseClient
|
|
stopChan chan bool
|
|
retryQueue map[string]int // Track retry count per certificate
|
|
maxRetries int
|
|
}
|
|
|
|
func NewSSLMonitoringService(pbClient *pocketbase.PocketBaseClient) *SSLMonitoringService {
|
|
return &SSLMonitoringService{
|
|
pbClient: pbClient,
|
|
stopChan: make(chan bool),
|
|
retryQueue: make(map[string]int),
|
|
maxRetries: 3,
|
|
}
|
|
}
|
|
|
|
func (s *SSLMonitoringService) Start() {
|
|
ticker := time.NewTicker(1 * time.Minute) // Check every minute for scheduling
|
|
defer ticker.Stop()
|
|
|
|
// log.Println("SSL monitoring service started with interval and check_at scheduling")
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
s.checkSSLCertificates()
|
|
case <-s.stopChan:
|
|
// log.Println("SSL monitoring service stopped")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *SSLMonitoringService) Stop() {
|
|
s.stopChan <- true
|
|
}
|
|
|
|
func (s *SSLMonitoringService) checkSSLCertificates() {
|
|
// log.Println("Fetching SSL certificates from PocketBase...")
|
|
|
|
certificates, err := s.pbClient.GetSSLCertificates()
|
|
if err != nil {
|
|
// log.Printf("Failed to fetch SSL certificates: %v", err)
|
|
_ = err
|
|
return
|
|
}
|
|
|
|
// log.Printf("Found %d SSL certificates to check", len(certificates))
|
|
|
|
for _, cert := range certificates {
|
|
if s.shouldCheckCertificate(cert) {
|
|
go s.checkSingleCertificateWithRetry(cert)
|
|
}
|
|
}
|
|
|
|
_ = certificates
|
|
}
|
|
|
|
func (s *SSLMonitoringService) shouldCheckCertificate(cert types.SSLCertificate) bool {
|
|
now := time.Now()
|
|
|
|
// Priority 1: Check if check_at is set and is due
|
|
if cert.CheckAt != "" {
|
|
if checkAt, err := s.parseFlexibleTime(cert.CheckAt); err == nil {
|
|
if now.After(checkAt) || now.Equal(checkAt) {
|
|
// log.Printf("Certificate %s is due for manual check (check_at: %s)",
|
|
// cert.Domain, checkAt.Format("2006-01-02 15:04:05"))
|
|
_ = checkAt
|
|
return true
|
|
} else {
|
|
// log.Printf("Certificate %s scheduled for later check (check_at: %s)",
|
|
// cert.Domain, checkAt.Format("2006-01-02 15:04:05"))
|
|
_ = checkAt
|
|
return false
|
|
}
|
|
} else {
|
|
// log.Printf("Error parsing check_at for %s: %v", cert.Domain, err)
|
|
_ = err
|
|
}
|
|
}
|
|
|
|
// Priority 2: Check based on check_interval (in days) from last update
|
|
if cert.Updated == "" {
|
|
// log.Printf("Certificate %s has never been checked, scheduling check", cert.Domain)
|
|
return true
|
|
}
|
|
|
|
// Parse last check time from updated field
|
|
lastCheck, err := s.parseFlexibleTime(cert.Updated)
|
|
if err != nil {
|
|
// log.Printf("Error parsing last check time for %s, scheduling check: %v", cert.Domain, err)
|
|
_ = err
|
|
return true
|
|
}
|
|
|
|
// Get check interval in days (default to 1 day if not set or invalid)
|
|
checkIntervalDays := cert.CheckInterval
|
|
if checkIntervalDays <= 0 {
|
|
checkIntervalDays = 1 // Default to 1 day
|
|
}
|
|
|
|
// Adjust check interval based on certificate status for critical certificates
|
|
adjustedIntervalDays := s.adjustCheckIntervalDays(cert, checkIntervalDays)
|
|
|
|
// Calculate next check time based on days
|
|
nextCheck := lastCheck.Add(time.Duration(adjustedIntervalDays) * 24 * time.Hour)
|
|
shouldCheck := now.After(nextCheck)
|
|
|
|
if shouldCheck {
|
|
// log.Printf("Certificate %s is due for interval check (last: %s, interval: %d days)",
|
|
// cert.Domain, lastCheck.Format("2006-01-02 15:04:05"), adjustedIntervalDays)
|
|
_ = lastCheck
|
|
_ = adjustedIntervalDays
|
|
} else {
|
|
// log.Printf("Certificate %s not due yet (next check: %s, interval: %d days)",
|
|
// cert.Domain, nextCheck.Format("2006-01-02 15:04:05"), adjustedIntervalDays)
|
|
_ = nextCheck
|
|
_ = adjustedIntervalDays
|
|
}
|
|
|
|
return shouldCheck
|
|
}
|
|
|
|
// parseFlexibleTime tries multiple time formats to parse timestamps
|
|
func (s *SSLMonitoringService) parseFlexibleTime(timeStr string) (time.Time, error) {
|
|
formats := []string{
|
|
time.RFC3339,
|
|
time.RFC3339Nano,
|
|
"2006-01-02 15:04:05.999Z", // ISO 8601 with milliseconds (PocketBase format)
|
|
"2006-01-02 15:04:05.999999Z", // ISO 8601 with microseconds
|
|
"2006-01-02 15:04:05Z", // ISO 8601 without milliseconds
|
|
"2006-01-02T15:04:05.999Z", // RFC3339 with milliseconds
|
|
"2006-01-02T15:04:05.999999Z", // RFC3339 with microseconds
|
|
"2006-01-02T15:04:05Z", // RFC3339 without milliseconds
|
|
"2006-01-02 15:04:05.999999999 -0700 MST",
|
|
"2006-01-02 15:04:05.999999 -0700 MST",
|
|
"2006-01-02 15:04:05 -0700 MST",
|
|
"2006-01-02 15:04:05.999999999",
|
|
"2006-01-02 15:04:05.999999",
|
|
"2006-01-02 15:04:05",
|
|
"2006-01-02T15:04:05.999999999Z",
|
|
}
|
|
|
|
for _, format := range formats {
|
|
if t, err := time.Parse(format, timeStr); err == nil {
|
|
return t, nil
|
|
}
|
|
}
|
|
|
|
return time.Time{}, fmt.Errorf("unable to parse time string: %s", timeStr)
|
|
}
|
|
|
|
// adjustCheckIntervalDays adjusts the check interval based on certificate status and days left
|
|
func (s *SSLMonitoringService) adjustCheckIntervalDays(cert types.SSLCertificate, defaultIntervalDays int) int {
|
|
// Check more frequently for certificates that are expiring soon or have errors
|
|
if cert.DaysLeft <= 7 {
|
|
return 1 // Check daily for certificates expiring within 7 days
|
|
} else if cert.DaysLeft <= 30 {
|
|
// Check every 2 days for certificates expiring within 30 days
|
|
if defaultIntervalDays > 2 {
|
|
return 2
|
|
}
|
|
} else if cert.Status == "error" {
|
|
return 1 // Check daily for certificates with errors
|
|
}
|
|
|
|
return defaultIntervalDays
|
|
}
|
|
|
|
func (s *SSLMonitoringService) checkSingleCertificateWithRetry(cert types.SSLCertificate) {
|
|
retryCount := s.retryQueue[cert.ID]
|
|
|
|
// log.Printf("🔍 Checking SSL certificate for domain: %s (attempt %d/%d)",
|
|
// cert.Domain, retryCount+1, s.maxRetries+1)
|
|
|
|
result, err := s.performSSLCheck(cert.Domain)
|
|
|
|
if err != nil && retryCount < s.maxRetries {
|
|
// Increment retry count and schedule retry
|
|
s.retryQueue[cert.ID] = retryCount + 1
|
|
// log.Printf("SSL check failed for %s, will retry (%d/%d): %v",
|
|
// cert.Domain, retryCount+1, s.maxRetries, err)
|
|
|
|
// Schedule retry with exponential backoff
|
|
go func() {
|
|
backoffDuration := time.Duration((retryCount + 1) * 30) * time.Second
|
|
time.Sleep(backoffDuration)
|
|
s.checkSingleCertificateWithRetry(cert)
|
|
}()
|
|
|
|
_ = retryCount
|
|
_ = err
|
|
return
|
|
}
|
|
|
|
// Reset retry count on success or max retries reached
|
|
delete(s.retryQueue, cert.ID)
|
|
|
|
if err != nil {
|
|
// log.Printf("❌ SSL check failed for domain %s after %d attempts: %v",
|
|
// cert.Domain, s.maxRetries+1, err)
|
|
s.updateCertificateWithError(cert, err)
|
|
return
|
|
}
|
|
|
|
// Update certificate with successful results
|
|
s.updateCertificateWithResults(cert, result)
|
|
}
|
|
|
|
func (s *SSLMonitoringService) performSSLCheck(domain string) (*types.OperationResult, error) {
|
|
// log.Printf("Performing SSL check for domain: %s", domain)
|
|
sslOp := operations.NewSSLOperation(30 * time.Second)
|
|
result, err := sslOp.Execute(domain)
|
|
|
|
if err != nil {
|
|
// log.Printf("SSL operation failed for %s: %v", domain, err)
|
|
_ = err
|
|
return nil, err
|
|
}
|
|
|
|
if result == nil {
|
|
// log.Printf("SSL operation returned nil result for %s", domain)
|
|
return nil, fmt.Errorf("SSL check returned nil result")
|
|
}
|
|
|
|
// log.Printf("SSL check completed for %s: success=%v, days_left=%d",
|
|
// domain, result.Success, result.SSLDaysLeft)
|
|
|
|
_ = domain
|
|
_ = result
|
|
return result, nil
|
|
}
|
|
|
|
func (s *SSLMonitoringService) updateCertificateWithError(cert types.SSLCertificate, err error) {
|
|
// log.Printf("Updating certificate %s with error status", cert.Domain)
|
|
|
|
updateData := map[string]interface{}{
|
|
"status": "error",
|
|
"updated": time.Now().Format(time.RFC3339),
|
|
"error_message": err.Error(),
|
|
}
|
|
|
|
// Calculate next check time based on check_interval (in days) with shorter interval for errors
|
|
checkIntervalDays := cert.CheckInterval
|
|
if checkIntervalDays <= 0 {
|
|
checkIntervalDays = 1
|
|
}
|
|
// For errors, check again in half the normal interval (minimum 1 day)
|
|
errorIntervalDays := checkIntervalDays / 2
|
|
if errorIntervalDays < 1 {
|
|
errorIntervalDays = 1
|
|
}
|
|
|
|
nextCheck := time.Now().Add(time.Duration(errorIntervalDays) * 24 * time.Hour)
|
|
updateData["check_at"] = nextCheck.Format(time.RFC3339)
|
|
|
|
if updateErr := s.pbClient.UpdateSSLCertificate(cert.ID, updateData); updateErr != nil {
|
|
// log.Printf("Failed to update SSL certificate %s with error status: %v", cert.ID, updateErr)
|
|
_ = updateErr
|
|
} else {
|
|
// log.Printf("📝 Updated certificate %s with error status (next check in %d days)",
|
|
// cert.Domain, errorIntervalDays)
|
|
_ = errorIntervalDays
|
|
}
|
|
|
|
_ = err
|
|
_ = updateData
|
|
_ = nextCheck
|
|
}
|
|
|
|
func (s *SSLMonitoringService) updateCertificateWithResults(cert types.SSLCertificate, result *types.OperationResult) {
|
|
status := getSSLStatus(result)
|
|
|
|
// log.Printf("Updating certificate %s with results: status=%s, days_left=%d, issuer=%s",
|
|
// cert.Domain, status, result.SSLDaysLeft, result.SSLIssuer)
|
|
|
|
updateData := map[string]interface{}{
|
|
"status": status,
|
|
"valid_from": result.SSLValidFrom.Format(time.RFC3339),
|
|
"valid_till": result.SSLValidTill.Format(time.RFC3339),
|
|
"days_left": result.SSLDaysLeft,
|
|
"valid_days_to_expire": result.SSLDaysLeft,
|
|
"resolved_ip": result.SSLResolvedIP,
|
|
"issuer_cn": result.SSLIssuer, // Now contains organization name like "Google Trust Services"
|
|
"issued_to": result.SSLSubject, // Now contains organization name
|
|
"serial_number": result.SSLSerialNumber,
|
|
"cert_alg": result.SSLAlgorithm,
|
|
"cert_sans": result.SSLSANs,
|
|
"updated": time.Now().Format(time.RFC3339),
|
|
"error_message": "", // Clear any previous error
|
|
}
|
|
|
|
// Calculate next check time based on check_interval (in days) and certificate status
|
|
checkIntervalDays := cert.CheckInterval
|
|
if checkIntervalDays <= 0 {
|
|
checkIntervalDays = 1 // Default to 1 day
|
|
}
|
|
|
|
adjustedIntervalDays := s.adjustCheckIntervalDays(cert, checkIntervalDays)
|
|
nextCheck := time.Now().Add(time.Duration(adjustedIntervalDays) * 24 * time.Hour)
|
|
updateData["check_at"] = nextCheck.Format(time.RFC3339)
|
|
|
|
if err := s.pbClient.UpdateSSLCertificate(cert.ID, updateData); err != nil {
|
|
// log.Printf("Failed to update SSL certificate %s: %v", cert.ID, err)
|
|
_ = err
|
|
} else {
|
|
// log.Printf("✅ SSL certificate updated for %s: %s (%d days left, issuer: %s, next check in %d days)",
|
|
// cert.Domain, status, result.SSLDaysLeft, result.SSLIssuer, adjustedIntervalDays)
|
|
_ = status
|
|
_ = result
|
|
_ = adjustedIntervalDays
|
|
}
|
|
|
|
_ = updateData
|
|
_ = nextCheck
|
|
}
|
|
|
|
func getSSLStatus(result *types.OperationResult) string {
|
|
if !result.Success {
|
|
return "error"
|
|
}
|
|
|
|
if result.SSLDaysLeft <= 0 {
|
|
return "expired"
|
|
} else if result.SSLDaysLeft <= 7 {
|
|
return "critical" // Very urgent
|
|
} else if result.SSLDaysLeft <= 30 {
|
|
return "expiring_soon"
|
|
}
|
|
|
|
return "valid"
|
|
} |