Add SSL certificate checker
Implement SSL certificate checking functionality
This commit is contained in:
@@ -8,10 +8,10 @@ A Go-based microservice for service operations including ICMP ping, DNS resoluti
|
||||
- **ICMP Ping**: Full ping functionality with packet statistics
|
||||
- **DNS Resolution**: A, AAAA, MX, and TXT record lookups
|
||||
- **TCP Connectivity**: Port connectivity testing
|
||||
- **SSL Certificate**: SSL Certificate Check
|
||||
- REST API endpoints
|
||||
- Health check endpoint
|
||||
- Configurable via environment variables
|
||||
- Docker support
|
||||
- Comprehensive operation statistics
|
||||
|
||||
## API Endpoints
|
||||
|
||||
@@ -21,7 +21,7 @@ type Config struct {
|
||||
|
||||
func Load() *Config {
|
||||
cfg := &Config{
|
||||
Port: getEnv("PORT", "8091"),
|
||||
Port: getEnv("PORT", "8092"),
|
||||
DefaultCount: getEnvInt("DEFAULT_COUNT", 4),
|
||||
DefaultTimeout: getEnvDuration("DEFAULT_TIMEOUT", 3*time.Second),
|
||||
MaxCount: getEnvInt("MAX_COUNT", 20),
|
||||
|
||||
@@ -79,6 +79,10 @@ func (h *OperationHandler) HandleOperation(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
result, err = httpOp.Execute(url, method)
|
||||
|
||||
case types.OperationSSL:
|
||||
sslOp := operations.NewSSLOperation(timeout)
|
||||
result, err = sslOp.Execute(req.Host)
|
||||
|
||||
default:
|
||||
http.Error(w, "Invalid operation type", http.StatusBadRequest)
|
||||
return
|
||||
|
||||
@@ -21,6 +21,7 @@ func main() {
|
||||
// Initialize PocketBase client (no credentials required)
|
||||
var pbClient *pocketbase.PocketBaseClient
|
||||
var monitoringService *monitoring.MonitoringService
|
||||
var sslMonitoringService *monitoring.SSLMonitoringService
|
||||
|
||||
if cfg.PocketBaseEnabled {
|
||||
var err error
|
||||
@@ -35,6 +36,11 @@ func main() {
|
||||
monitoringService = monitoring.NewMonitoringService(pbClient)
|
||||
go monitoringService.Start()
|
||||
log.Println("Monitoring service started (public access mode)")
|
||||
|
||||
// Initialize and start SSL monitoring service
|
||||
sslMonitoringService = monitoring.NewSSLMonitoringService(pbClient)
|
||||
go sslMonitoringService.Start()
|
||||
log.Println("SSL monitoring service started")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,13 +69,16 @@ func main() {
|
||||
if monitoringService != nil {
|
||||
log.Printf("Automatic service monitoring enabled")
|
||||
}
|
||||
if sslMonitoringService != nil {
|
||||
log.Printf("SSL certificate monitoring enabled")
|
||||
}
|
||||
log.Printf("Endpoints:")
|
||||
log.Printf(" POST /operation - Full operation test (ping, dns, tcp, http)")
|
||||
log.Printf(" POST /operation - Full operation test (ping, dns, tcp, http, ssl)")
|
||||
log.Printf(" GET /operation/quick?type=<type>&host=<host> - Quick operation test")
|
||||
log.Printf(" POST /ping - Legacy ping endpoint")
|
||||
log.Printf(" GET /ping/quick?host=<host> - Legacy quick ping test")
|
||||
log.Printf(" GET /health - Health check")
|
||||
log.Printf("Supported operations: ping, dns, tcp, http")
|
||||
log.Printf("Supported operations: ping, dns, tcp, http, ssl")
|
||||
|
||||
// Setup graceful shutdown
|
||||
c := make(chan os.Signal, 1)
|
||||
@@ -77,10 +86,13 @@ func main() {
|
||||
|
||||
go func() {
|
||||
<-c
|
||||
log.Println("Shutting down monitoring service...")
|
||||
log.Println("Shutting down monitoring services...")
|
||||
if monitoringService != nil {
|
||||
monitoringService.Stop()
|
||||
}
|
||||
if sslMonitoringService != nil {
|
||||
sslMonitoringService.Stop()
|
||||
}
|
||||
log.Println("Service stopped")
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
package monitoring
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"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)
|
||||
return
|
||||
}
|
||||
|
||||
//log.Printf("Found %d SSL certificates to check", len(certificates))
|
||||
|
||||
for _, cert := range certificates {
|
||||
if s.shouldCheckCertificate(cert) {
|
||||
go s.checkSingleCertificateWithRetry(cert)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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"))
|
||||
return true
|
||||
} else {
|
||||
//log.Printf("Certificate %s scheduled for later check (check_at: %s)",
|
||||
//cert.Domain, checkAt.Format("2006-01-02 15:04:05"))
|
||||
//return false
|
||||
}
|
||||
} else {
|
||||
log.Printf("Error parsing check_at for %s: %v", cert.Domain, 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)
|
||||
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)
|
||||
} 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)
|
||||
}
|
||||
|
||||
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)
|
||||
}()
|
||||
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)
|
||||
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)
|
||||
|
||||
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)
|
||||
} else {
|
||||
log.Printf("📝 Updated certificate %s with error status (next check in %d days)",
|
||||
cert.Domain, errorIntervalDays)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
|
||||
package operations
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
// extractSANs extracts Subject Alternative Names from certificate
|
||||
func (op *SSLOperation) extractSANs(cert *x509.Certificate) []string {
|
||||
sans := make([]string, 0)
|
||||
|
||||
// Add DNS names
|
||||
sans = append(sans, cert.DNSNames...)
|
||||
|
||||
// Add IP addresses
|
||||
for _, ip := range cert.IPAddresses {
|
||||
sans = append(sans, ip.String())
|
||||
}
|
||||
|
||||
// Add email addresses
|
||||
sans = append(sans, cert.EmailAddresses...)
|
||||
|
||||
// Add URIs
|
||||
for _, uri := range cert.URIs {
|
||||
sans = append(sans, uri.String())
|
||||
}
|
||||
|
||||
return sans
|
||||
}
|
||||
|
||||
// getResolvedIP resolves the domain to its IP address
|
||||
func (op *SSLOperation) getResolvedIP(hostname string) string {
|
||||
ips, err := net.LookupIP(hostname)
|
||||
if err != nil || len(ips) == 0 {
|
||||
return ""
|
||||
}
|
||||
return ips[0].String()
|
||||
}
|
||||
|
||||
// getCertificateAlgorithm returns detailed algorithm information
|
||||
func (op *SSLOperation) getCertificateAlgorithm(cert *x509.Certificate) string {
|
||||
algorithm := cert.SignatureAlgorithm.String()
|
||||
|
||||
// Add key size information if available
|
||||
switch pub := cert.PublicKey.(type) {
|
||||
case *rsa.PublicKey:
|
||||
algorithm += fmt.Sprintf(" (RSA %d-bit)", pub.N.BitLen())
|
||||
case *ecdsa.PublicKey:
|
||||
algorithm += fmt.Sprintf(" (ECDSA %d-bit)", pub.Curve.Params().BitSize)
|
||||
}
|
||||
|
||||
return algorithm
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
|
||||
package operations
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"service-operation/types"
|
||||
)
|
||||
|
||||
type SSLOperation struct {
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func NewSSLOperation(timeout time.Duration) *SSLOperation {
|
||||
return &SSLOperation{
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (op *SSLOperation) Execute(domain string) (*types.OperationResult, error) {
|
||||
startTime := time.Now()
|
||||
|
||||
// Clean and normalize domain
|
||||
domain = op.normalizeDomain(domain)
|
||||
|
||||
// Validate domain format
|
||||
if domain == "" {
|
||||
return op.createErrorResult(domain, startTime, "domain cannot be empty")
|
||||
}
|
||||
|
||||
// Add port if not present
|
||||
host := domain
|
||||
if !strings.Contains(host, ":") {
|
||||
host = host + ":443"
|
||||
}
|
||||
|
||||
// Set up TLS connection with timeout
|
||||
dialer := &net.Dialer{
|
||||
Timeout: op.timeout,
|
||||
}
|
||||
|
||||
// Create TLS config with proper verification
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: strings.Split(host, ":")[0],
|
||||
InsecureSkipVerify: false,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
|
||||
// Attempt TLS connection
|
||||
conn, err := tls.DialWithDialer(dialer, "tcp", host, tlsConfig)
|
||||
|
||||
endTime := time.Now()
|
||||
responseTime := endTime.Sub(startTime)
|
||||
|
||||
if err != nil {
|
||||
return &types.OperationResult{
|
||||
Type: types.OperationSSL,
|
||||
Host: strings.Split(host, ":")[0],
|
||||
Success: false,
|
||||
ResponseTime: responseTime,
|
||||
Error: fmt.Sprintf("TLS connection failed: %v", err),
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
}, nil
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Get certificate chain information
|
||||
state := conn.ConnectionState()
|
||||
if len(state.PeerCertificates) == 0 {
|
||||
return &types.OperationResult{
|
||||
Type: types.OperationSSL,
|
||||
Host: strings.Split(host, ":")[0],
|
||||
Success: false,
|
||||
ResponseTime: responseTime,
|
||||
Error: "No certificates found in chain",
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
cert := state.PeerCertificates[0]
|
||||
hostname := strings.Split(host, ":")[0]
|
||||
|
||||
// Perform comprehensive certificate validation
|
||||
validationError := op.validateCertificate(cert, hostname)
|
||||
|
||||
// Calculate days left until expiration
|
||||
daysLeft := int(time.Until(cert.NotAfter).Hours() / 24)
|
||||
|
||||
// Extract Subject Alternative Names
|
||||
sans := op.extractSANs(cert)
|
||||
|
||||
// Get resolved IP address
|
||||
resolvedIP := op.getResolvedIP(hostname)
|
||||
|
||||
// Extract certificate algorithm information
|
||||
algorithm := op.getCertificateAlgorithm(cert)
|
||||
|
||||
// Extract issuer organization (O=) instead of full distinguished name
|
||||
issuerOrganization := op.extractIssuerOrganization(cert.Issuer)
|
||||
|
||||
// Extract subject organization (O=) instead of full distinguished name
|
||||
subjectOrganization := op.extractSubjectOrganization(cert.Subject)
|
||||
|
||||
// Check if certificate is valid (not expired and passes validation)
|
||||
isValid := validationError == nil && time.Now().Before(cert.NotAfter) && time.Now().After(cert.NotBefore)
|
||||
|
||||
// Build detailed error message if validation failed
|
||||
errorMsg := ""
|
||||
if validationError != nil {
|
||||
errorMsg = validationError.Error()
|
||||
} else if time.Now().After(cert.NotAfter) {
|
||||
errorMsg = "Certificate has expired"
|
||||
} else if time.Now().Before(cert.NotBefore) {
|
||||
errorMsg = "Certificate is not yet valid"
|
||||
}
|
||||
|
||||
// Create comprehensive result
|
||||
result := &types.OperationResult{
|
||||
Type: types.OperationSSL,
|
||||
Host: hostname,
|
||||
Success: isValid,
|
||||
ResponseTime: responseTime,
|
||||
Error: errorMsg,
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
|
||||
// SSL specific fields - using organization instead of full DN
|
||||
SSLValidFrom: cert.NotBefore,
|
||||
SSLValidTill: cert.NotAfter,
|
||||
SSLDaysLeft: daysLeft,
|
||||
SSLIssuer: issuerOrganization, // Now shows "Google Trust Services" instead of full DN
|
||||
SSLSubject: subjectOrganization, // Now shows organization instead of full DN
|
||||
SSLSerialNumber: cert.SerialNumber.String(),
|
||||
SSLAlgorithm: algorithm,
|
||||
SSLSANs: strings.Join(sans, ","),
|
||||
SSLResolvedIP: resolvedIP,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// createErrorResult creates a standardized error result
|
||||
func (op *SSLOperation) createErrorResult(domain string, startTime time.Time, errorMsg string) (*types.OperationResult, error) {
|
||||
return &types.OperationResult{
|
||||
Type: types.OperationSSL,
|
||||
Host: domain,
|
||||
Success: false,
|
||||
ResponseTime: time.Since(startTime),
|
||||
Error: errorMsg,
|
||||
StartTime: startTime,
|
||||
EndTime: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
|
||||
package operations
|
||||
|
||||
import (
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// normalizeDomain cleans and normalizes the domain input
|
||||
func (op *SSLOperation) normalizeDomain(domain string) string {
|
||||
// Remove protocol prefixes
|
||||
domain = strings.Replace(domain, "https://", "", 1)
|
||||
domain = strings.Replace(domain, "http://", "", 1)
|
||||
|
||||
// Remove trailing slash and path
|
||||
if idx := strings.Index(domain, "/"); idx != -1 {
|
||||
domain = domain[:idx]
|
||||
}
|
||||
|
||||
// Trim whitespace
|
||||
domain = strings.TrimSpace(domain)
|
||||
|
||||
return domain
|
||||
}
|
||||
|
||||
// formatDistinguishedName formats the certificate distinguished name
|
||||
func (op *SSLOperation) formatDistinguishedName(name pkix.Name) string {
|
||||
var parts []string
|
||||
|
||||
if name.CommonName != "" {
|
||||
parts = append(parts, fmt.Sprintf("CN=%s", name.CommonName))
|
||||
}
|
||||
|
||||
for _, org := range name.Organization {
|
||||
parts = append(parts, fmt.Sprintf("O=%s", org))
|
||||
}
|
||||
|
||||
for _, orgUnit := range name.OrganizationalUnit {
|
||||
parts = append(parts, fmt.Sprintf("OU=%s", orgUnit))
|
||||
}
|
||||
|
||||
for _, country := range name.Country {
|
||||
parts = append(parts, fmt.Sprintf("C=%s", country))
|
||||
}
|
||||
|
||||
for _, locality := range name.Locality {
|
||||
parts = append(parts, fmt.Sprintf("L=%s", locality))
|
||||
}
|
||||
|
||||
for _, province := range name.Province {
|
||||
parts = append(parts, fmt.Sprintf("ST=%s", province))
|
||||
}
|
||||
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
// extractIssuerOrganization extracts only the organization (O=) from the issuer distinguished name
|
||||
func (op *SSLOperation) extractIssuerOrganization(name pkix.Name) string {
|
||||
// Return the first organization if available
|
||||
if len(name.Organization) > 0 {
|
||||
return name.Organization[0]
|
||||
}
|
||||
|
||||
// Fallback to Common Name if no organization
|
||||
if name.CommonName != "" {
|
||||
return name.CommonName
|
||||
}
|
||||
|
||||
// Last resort fallback
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
// extractSubjectOrganization extracts only the organization (O=) from the subject distinguished name
|
||||
func (op *SSLOperation) extractSubjectOrganization(name pkix.Name) string {
|
||||
// Return the first organization if available
|
||||
if len(name.Organization) > 0 {
|
||||
return name.Organization[0]
|
||||
}
|
||||
|
||||
// Fallback to Common Name if no organization
|
||||
if name.CommonName != "" {
|
||||
return name.CommonName
|
||||
}
|
||||
|
||||
// Last resort fallback
|
||||
return "Unknown"
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package operations
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// validateCertificate performs comprehensive certificate validation
|
||||
func (op *SSLOperation) validateCertificate(cert *x509.Certificate, hostname string) error {
|
||||
now := time.Now()
|
||||
|
||||
// Check if certificate is expired or not yet valid
|
||||
if now.Before(cert.NotBefore) {
|
||||
return fmt.Errorf("certificate is not yet valid (valid from: %v)", cert.NotBefore.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
if now.After(cert.NotAfter) {
|
||||
return fmt.Errorf("certificate has expired (expired on: %v)", cert.NotAfter.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
|
||||
// Verify hostname matches certificate
|
||||
if err := cert.VerifyHostname(hostname); err != nil {
|
||||
return fmt.Errorf("hostname verification failed: %v", err)
|
||||
}
|
||||
|
||||
// Check key usage - certificates should have digital signature capability
|
||||
if cert.KeyUsage&x509.KeyUsageDigitalSignature == 0 {
|
||||
return fmt.Errorf("certificate missing required digital signature key usage")
|
||||
}
|
||||
|
||||
// Check if certificate is self-signed (basic check)
|
||||
if cert.Issuer.CommonName == cert.Subject.CommonName && len(cert.Subject.Organization) == 0 {
|
||||
return fmt.Errorf("certificate appears to be self-signed")
|
||||
}
|
||||
|
||||
// Validate certificate chain if intermediate certificates are present
|
||||
if err := op.validateCertificateChain(cert); err != nil {
|
||||
return fmt.Errorf("certificate chain validation failed: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateCertificateChain performs basic certificate chain validation
|
||||
func (op *SSLOperation) validateCertificateChain(cert *x509.Certificate) error {
|
||||
// Check if the certificate has proper extensions for SSL/TLS
|
||||
hasServerAuth := false
|
||||
for _, usage := range cert.ExtKeyUsage {
|
||||
if usage == x509.ExtKeyUsageServerAuth {
|
||||
hasServerAuth = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasServerAuth {
|
||||
return fmt.Errorf("certificate does not have server authentication extension")
|
||||
}
|
||||
|
||||
// Check certificate version (should be v3 for modern certificates)
|
||||
if cert.Version < 3 {
|
||||
return fmt.Errorf("certificate version %d is outdated (should be v3)", cert.Version)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
|
||||
package pocketbase
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"service-operation/types"
|
||||
)
|
||||
|
||||
type SSLCertificatesResponse struct {
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"perPage"`
|
||||
TotalItems int `json:"totalItems"`
|
||||
TotalPages int `json:"totalPages"`
|
||||
Items []types.SSLCertificate `json:"items"`
|
||||
}
|
||||
|
||||
func (c *PocketBaseClient) GetSSLCertificates() ([]types.SSLCertificate, error) {
|
||||
url := fmt.Sprintf("%s/api/collections/ssl_certificates/records", c.baseURL)
|
||||
|
||||
resp, err := c.httpClient.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch SSL certificates: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("PocketBase returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var response SSLCertificatesResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode SSL certificates response: %v", err)
|
||||
}
|
||||
|
||||
return response.Items, nil
|
||||
}
|
||||
|
||||
func (c *PocketBaseClient) UpdateSSLCertificate(id string, data map[string]interface{}) error {
|
||||
url := fmt.Sprintf("%s/api/collections/ssl_certificates/records/%s", c.baseURL, id)
|
||||
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal SSL certificate data: %v", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPatch, url, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create SSL certificate update request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update SSL certificate: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("failed to update SSL certificate, status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *PocketBaseClient) GetSSLCertificateByID(id string) (*types.SSLCertificate, error) {
|
||||
url := fmt.Sprintf("%s/api/collections/ssl_certificates/records/%s", c.baseURL, id)
|
||||
|
||||
resp, err := c.httpClient.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch SSL certificate: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("PocketBase returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var cert types.SSLCertificate
|
||||
if err := json.NewDecoder(resp.Body).Decode(&cert); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode SSL certificate response: %v", err)
|
||||
}
|
||||
|
||||
return &cert, nil
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
package types
|
||||
|
||||
import "time"
|
||||
@@ -10,6 +9,7 @@ const (
|
||||
OperationDNS OperationType = "dns"
|
||||
OperationTCP OperationType = "tcp"
|
||||
OperationHTTP OperationType = "http"
|
||||
OperationSSL OperationType = "ssl"
|
||||
)
|
||||
|
||||
type OperationRequest struct {
|
||||
@@ -56,6 +56,17 @@ type OperationResult struct {
|
||||
ContentLength int64 `json:"content_length,omitempty"`
|
||||
ResponseBody string `json:"response_body,omitempty"`
|
||||
|
||||
// SSL specific fields
|
||||
SSLValidFrom time.Time `json:"ssl_valid_from,omitempty"`
|
||||
SSLValidTill time.Time `json:"ssl_valid_till,omitempty"`
|
||||
SSLDaysLeft int `json:"ssl_days_left,omitempty"`
|
||||
SSLIssuer string `json:"ssl_issuer,omitempty"`
|
||||
SSLSubject string `json:"ssl_subject,omitempty"`
|
||||
SSLSerialNumber string `json:"ssl_serial_number,omitempty"`
|
||||
SSLAlgorithm string `json:"ssl_algorithm,omitempty"`
|
||||
SSLSANs string `json:"ssl_sans,omitempty"`
|
||||
SSLResolvedIP string `json:"ssl_resolved_ip,omitempty"`
|
||||
|
||||
StartTime time.Time `json:"start_time"`
|
||||
EndTime time.Time `json:"end_time"`
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SSLCertificate struct {
|
||||
ID string `json:"id"`
|
||||
CollectionID string `json:"collectionId"`
|
||||
CollectionName string `json:"collectionName"`
|
||||
Domain string `json:"domain"`
|
||||
IssuerO string `json:"issuer_o"`
|
||||
Status string `json:"status"`
|
||||
LastNotified string `json:"last_notified"`
|
||||
WarningThreshold int `json:"warning_threshold"`
|
||||
ExpiryThreshold int `json:"expiry_threshold"`
|
||||
NotificationChannel string `json:"notification_channel"`
|
||||
ValidFrom string `json:"valid_from"`
|
||||
SerialNumber string `json:"serial_number"` // Changed to string to handle large numbers
|
||||
IssuedTo string `json:"issued_to"`
|
||||
ValidTill string `json:"valid_till"`
|
||||
ValidityDays int `json:"validity_days"`
|
||||
DaysLeft int `json:"days_left"`
|
||||
ValidDaysToExpire int `json:"valid_days_to_expire"`
|
||||
ResolvedIP string `json:"resolved_ip"`
|
||||
IssuerCN string `json:"issuer_cn"`
|
||||
CertAlg string `json:"cert_alg"`
|
||||
CertSans string `json:"cert_sans"`
|
||||
CheckInterval int `json:"check_interval"`
|
||||
CheckAt string `json:"check_at"`
|
||||
Created string `json:"created"`
|
||||
Updated string `json:"updated"`
|
||||
}
|
||||
|
||||
// Custom unmarshaler to handle check_interval as both string and int, and serial_number as string
|
||||
func (s *SSLCertificate) UnmarshalJSON(data []byte) error {
|
||||
// Create a temporary struct with flexible types
|
||||
type Alias SSLCertificate
|
||||
aux := &struct {
|
||||
CheckInterval interface{} `json:"check_interval"`
|
||||
SerialNumber interface{} `json:"serial_number"`
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(s),
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Handle check_interval conversion
|
||||
switch v := aux.CheckInterval.(type) {
|
||||
case string:
|
||||
if v == "" {
|
||||
s.CheckInterval = 1440 // Default 24 hours in minutes
|
||||
} else {
|
||||
interval, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
s.CheckInterval = 1440 // Default on error
|
||||
} else {
|
||||
s.CheckInterval = interval
|
||||
}
|
||||
}
|
||||
case float64:
|
||||
s.CheckInterval = int(v)
|
||||
case int:
|
||||
s.CheckInterval = v
|
||||
default:
|
||||
s.CheckInterval = 1440 // Default 24 hours in minutes
|
||||
}
|
||||
|
||||
// Handle serial_number conversion to string
|
||||
switch v := aux.SerialNumber.(type) {
|
||||
case string:
|
||||
s.SerialNumber = v
|
||||
case float64:
|
||||
// Handle scientific notation by converting to string
|
||||
s.SerialNumber = strconv.FormatFloat(v, 'f', 0, 64)
|
||||
case int64:
|
||||
s.SerialNumber = strconv.FormatInt(v, 10)
|
||||
case int:
|
||||
s.SerialNumber = strconv.Itoa(v)
|
||||
default:
|
||||
s.SerialNumber = ""
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type SSLCheckResult struct {
|
||||
Domain string `json:"domain"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
ValidFrom time.Time `json:"valid_from"`
|
||||
ValidTill time.Time `json:"valid_till"`
|
||||
DaysLeft int `json:"days_left"`
|
||||
Issuer string `json:"issuer"`
|
||||
Subject string `json:"subject"`
|
||||
SerialNumber string `json:"serial_number"`
|
||||
Algorithm string `json:"algorithm"`
|
||||
SANs []string `json:"sans"`
|
||||
ResponseTime time.Duration `json:"response_time"`
|
||||
ResolvedIP string `json:"resolved_ip"`
|
||||
CheckedAt time.Time `json:"checked_at"`
|
||||
}
|
||||
Reference in New Issue
Block a user