mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 16:31:33 +00:00
21aeed4f4e
Phase 0 closure (Path B2, post-rewrite):
addlicense sweep — adds the canonical certctl LLC copyright + BUSL-1.1
SPDX header to every production Go file. Template:
// Copyright 2026 certctl LLC. All rights reserved.
// SPDX-License-Identifier: BUSL-1.1
Coverage: 338 / 338 production Go files (cmd/ + internal/, excluding
*_test.go and **/testdata/**). Pre-sweep coverage was 22 / 338 (6.5%);
post-sweep is 338 / 338 (100%).
Normalized 22 pre-existing legacy headers (`// Copyright (c) certctl`
+ `// SPDX-License-Identifier: BSL-1.1`) and 1 file using a
`Certctl Contributors` attribution. The legacy SPDX ID `BSL-1.1`
is non-standard; the official SPDX identifier for Business Source
License 1.1 is `BUSL-1.1` (capital U). All 338 files now share the
canonical form.
Generated via:
addlicense -c "certctl LLC" -y 2026 \
-f cowork/legal/copyright-header.tpl \
-ignore '**/testdata/**' -ignore '**/*_test.go' \
cmd/ internal/
Verification:
find cmd internal -name '*.go' -not -name '*_test.go' \
-not -path '*/testdata/*' \
-exec grep -L '^// Copyright 2026 certctl LLC' {} \; | wc -l
Returns: 0
gofmt clean. Header additions are comments only, no compile impact.
Closes: cowork/certctl-architecture-diligence-audit.html#fix-RED-4
457 lines
16 KiB
Go
457 lines
16 KiB
Go
// Copyright 2026 certctl LLC. All rights reserved.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/certctl-io/certctl/internal/domain"
|
|
"github.com/certctl-io/certctl/internal/repository"
|
|
)
|
|
|
|
// HealthCheckRepository implements repository.HealthCheckRepository using PostgreSQL.
|
|
type HealthCheckRepository struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
// NewHealthCheckRepository creates a new PostgreSQL-backed health check repository.
|
|
func NewHealthCheckRepository(db *sql.DB) *HealthCheckRepository {
|
|
return &HealthCheckRepository{db: db}
|
|
}
|
|
|
|
// Create stores a new health check.
|
|
func (r *HealthCheckRepository) Create(ctx context.Context, check *domain.EndpointHealthCheck) error {
|
|
_, err := r.db.ExecContext(ctx, `
|
|
INSERT INTO endpoint_health_checks (
|
|
id, endpoint, certificate_id, network_scan_target_id,
|
|
expected_fingerprint, observed_fingerprint, status,
|
|
consecutive_failures, response_time_ms, tls_version, cipher_suite,
|
|
cert_subject, cert_issuer, cert_expiry,
|
|
last_checked_at, last_success_at, last_failure_at, last_transition_at,
|
|
failure_reason, degraded_threshold, down_threshold, check_interval_seconds,
|
|
enabled, acknowledged, acknowledged_by, acknowledged_at,
|
|
created_at, updated_at
|
|
) VALUES (
|
|
$1, $2, $3, $4,
|
|
$5, $6, $7,
|
|
$8, $9, $10, $11,
|
|
$12, $13, $14,
|
|
$15, $16, $17, $18,
|
|
$19, $20, $21, $22,
|
|
$23, $24, $25, $26,
|
|
$27, $28
|
|
)`,
|
|
check.ID, check.Endpoint, check.CertificateID, check.NetworkScanTargetID,
|
|
check.ExpectedFingerprint, check.ObservedFingerprint, string(check.Status),
|
|
check.ConsecutiveFailures, check.ResponseTimeMs, check.TLSVersion, check.CipherSuite,
|
|
check.CertSubject, check.CertIssuer, check.CertExpiry,
|
|
check.LastCheckedAt, check.LastSuccessAt, check.LastFailureAt, check.LastTransitionAt,
|
|
check.FailureReason, check.DegradedThreshold, check.DownThreshold, check.CheckIntervalSecs,
|
|
check.Enabled, check.Acknowledged, check.AcknowledgedBy, check.AcknowledgedAt,
|
|
check.CreatedAt, check.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("create health check: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Update modifies an existing health check.
|
|
func (r *HealthCheckRepository) Update(ctx context.Context, check *domain.EndpointHealthCheck) error {
|
|
check.UpdatedAt = time.Now()
|
|
_, err := r.db.ExecContext(ctx, `
|
|
UPDATE endpoint_health_checks SET
|
|
endpoint = $2, certificate_id = $3, network_scan_target_id = $4,
|
|
expected_fingerprint = $5, observed_fingerprint = $6, status = $7,
|
|
consecutive_failures = $8, response_time_ms = $9, tls_version = $10, cipher_suite = $11,
|
|
cert_subject = $12, cert_issuer = $13, cert_expiry = $14,
|
|
last_checked_at = $15, last_success_at = $16, last_failure_at = $17, last_transition_at = $18,
|
|
failure_reason = $19, degraded_threshold = $20, down_threshold = $21, check_interval_seconds = $22,
|
|
enabled = $23, acknowledged = $24, acknowledged_by = $25, acknowledged_at = $26,
|
|
updated_at = $27
|
|
WHERE id = $1`,
|
|
check.ID,
|
|
check.Endpoint, check.CertificateID, check.NetworkScanTargetID,
|
|
check.ExpectedFingerprint, check.ObservedFingerprint, string(check.Status),
|
|
check.ConsecutiveFailures, check.ResponseTimeMs, check.TLSVersion, check.CipherSuite,
|
|
check.CertSubject, check.CertIssuer, check.CertExpiry,
|
|
check.LastCheckedAt, check.LastSuccessAt, check.LastFailureAt, check.LastTransitionAt,
|
|
check.FailureReason, check.DegradedThreshold, check.DownThreshold, check.CheckIntervalSecs,
|
|
check.Enabled, check.Acknowledged, check.AcknowledgedBy, check.AcknowledgedAt,
|
|
check.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("update health check: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Get retrieves a health check by ID.
|
|
func (r *HealthCheckRepository) Get(ctx context.Context, id string) (*domain.EndpointHealthCheck, error) {
|
|
check := &domain.EndpointHealthCheck{}
|
|
var status string
|
|
var certExpiry, lastCheckedAt, lastSuccessAt, lastFailureAt, lastTransitionAt, acknowledgedAt sql.NullTime
|
|
err := r.db.QueryRowContext(ctx, `
|
|
SELECT id, endpoint, certificate_id, network_scan_target_id,
|
|
expected_fingerprint, observed_fingerprint, status,
|
|
consecutive_failures, response_time_ms, tls_version, cipher_suite,
|
|
cert_subject, cert_issuer, cert_expiry,
|
|
last_checked_at, last_success_at, last_failure_at, last_transition_at,
|
|
failure_reason, degraded_threshold, down_threshold, check_interval_seconds,
|
|
enabled, acknowledged, acknowledged_by, acknowledged_at,
|
|
created_at, updated_at
|
|
FROM endpoint_health_checks
|
|
WHERE id = $1`, id).Scan(
|
|
&check.ID, &check.Endpoint, &check.CertificateID, &check.NetworkScanTargetID,
|
|
&check.ExpectedFingerprint, &check.ObservedFingerprint, &status,
|
|
&check.ConsecutiveFailures, &check.ResponseTimeMs, &check.TLSVersion, &check.CipherSuite,
|
|
&check.CertSubject, &check.CertIssuer, &certExpiry,
|
|
&lastCheckedAt, &lastSuccessAt, &lastFailureAt, &lastTransitionAt,
|
|
&check.FailureReason, &check.DegradedThreshold, &check.DownThreshold, &check.CheckIntervalSecs,
|
|
&check.Enabled, &check.Acknowledged, &check.AcknowledgedBy, &acknowledgedAt,
|
|
&check.CreatedAt, &check.UpdatedAt,
|
|
)
|
|
if err == sql.ErrNoRows {
|
|
return nil, fmt.Errorf("health check not found: %w", repository.ErrNotFound)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get health check: %w", err)
|
|
}
|
|
check.Status = domain.HealthStatus(status)
|
|
if certExpiry.Valid {
|
|
check.CertExpiry = &certExpiry.Time
|
|
}
|
|
if lastCheckedAt.Valid {
|
|
check.LastCheckedAt = &lastCheckedAt.Time
|
|
}
|
|
if lastSuccessAt.Valid {
|
|
check.LastSuccessAt = &lastSuccessAt.Time
|
|
}
|
|
if lastFailureAt.Valid {
|
|
check.LastFailureAt = &lastFailureAt.Time
|
|
}
|
|
if lastTransitionAt.Valid {
|
|
check.LastTransitionAt = &lastTransitionAt.Time
|
|
}
|
|
if acknowledgedAt.Valid {
|
|
check.AcknowledgedAt = &acknowledgedAt.Time
|
|
}
|
|
return check, nil
|
|
}
|
|
|
|
// Delete removes a health check.
|
|
func (r *HealthCheckRepository) Delete(ctx context.Context, id string) error {
|
|
_, err := r.db.ExecContext(ctx, `DELETE FROM endpoint_health_checks WHERE id = $1`, id)
|
|
if err != nil {
|
|
return fmt.Errorf("delete health check: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// List returns health checks matching the filter with pagination.
|
|
func (r *HealthCheckRepository) List(ctx context.Context, filter *repository.HealthCheckFilter) ([]*domain.EndpointHealthCheck, int, error) {
|
|
query := `SELECT id, endpoint, certificate_id, network_scan_target_id,
|
|
expected_fingerprint, observed_fingerprint, status,
|
|
consecutive_failures, response_time_ms, tls_version, cipher_suite,
|
|
cert_subject, cert_issuer, cert_expiry,
|
|
last_checked_at, last_success_at, last_failure_at, last_transition_at,
|
|
failure_reason, degraded_threshold, down_threshold, check_interval_seconds,
|
|
enabled, acknowledged, acknowledged_by, acknowledged_at,
|
|
created_at, updated_at
|
|
FROM endpoint_health_checks`
|
|
countQuery := `SELECT COUNT(*) FROM endpoint_health_checks`
|
|
|
|
var conditions []string
|
|
var args []interface{}
|
|
argIdx := 1
|
|
|
|
if filter != nil {
|
|
if filter.Status != "" {
|
|
conditions = append(conditions, fmt.Sprintf("status = $%d", argIdx))
|
|
args = append(args, filter.Status)
|
|
argIdx++
|
|
}
|
|
if filter.CertificateID != "" {
|
|
conditions = append(conditions, fmt.Sprintf("certificate_id = $%d", argIdx))
|
|
args = append(args, filter.CertificateID)
|
|
argIdx++
|
|
}
|
|
if filter.NetworkScanTargetID != "" {
|
|
conditions = append(conditions, fmt.Sprintf("network_scan_target_id = $%d", argIdx))
|
|
args = append(args, filter.NetworkScanTargetID)
|
|
argIdx++
|
|
}
|
|
if filter.Enabled != nil {
|
|
conditions = append(conditions, fmt.Sprintf("enabled = $%d", argIdx))
|
|
args = append(args, *filter.Enabled)
|
|
argIdx++
|
|
}
|
|
}
|
|
|
|
if len(conditions) > 0 {
|
|
where := " WHERE " + conditions[0]
|
|
for i := 1; i < len(conditions); i++ {
|
|
where += " AND " + conditions[i]
|
|
}
|
|
query += where
|
|
countQuery += where
|
|
}
|
|
|
|
// Get total count
|
|
var total int
|
|
err := r.db.QueryRowContext(ctx, countQuery, args...).Scan(&total)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("count health checks: %w", err)
|
|
}
|
|
|
|
// Apply pagination
|
|
query += " ORDER BY created_at DESC"
|
|
page := 1
|
|
perPage := 50
|
|
if filter != nil {
|
|
if filter.Page > 0 {
|
|
page = filter.Page
|
|
}
|
|
if filter.PerPage > 0 {
|
|
perPage = filter.PerPage
|
|
}
|
|
}
|
|
offset := (page - 1) * perPage
|
|
query += fmt.Sprintf(" LIMIT $%d OFFSET $%d", argIdx, argIdx+1)
|
|
args = append(args, perPage, offset)
|
|
|
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("list health checks: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var checks []*domain.EndpointHealthCheck
|
|
for rows.Next() {
|
|
check, err := scanHealthCheck(rows)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
checks = append(checks, check)
|
|
}
|
|
return checks, total, rows.Err()
|
|
}
|
|
|
|
// ListDueForCheck returns health checks where the check interval has been exceeded.
|
|
func (r *HealthCheckRepository) ListDueForCheck(ctx context.Context) ([]*domain.EndpointHealthCheck, error) {
|
|
rows, err := r.db.QueryContext(ctx, `
|
|
SELECT id, endpoint, certificate_id, network_scan_target_id,
|
|
expected_fingerprint, observed_fingerprint, status,
|
|
consecutive_failures, response_time_ms, tls_version, cipher_suite,
|
|
cert_subject, cert_issuer, cert_expiry,
|
|
last_checked_at, last_success_at, last_failure_at, last_transition_at,
|
|
failure_reason, degraded_threshold, down_threshold, check_interval_seconds,
|
|
enabled, acknowledged, acknowledged_by, acknowledged_at,
|
|
created_at, updated_at
|
|
FROM endpoint_health_checks
|
|
WHERE enabled = TRUE
|
|
AND (
|
|
last_checked_at IS NULL
|
|
OR last_checked_at + (check_interval_seconds * INTERVAL '1 second') < NOW()
|
|
)
|
|
ORDER BY last_checked_at ASC NULLS FIRST`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list due health checks: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var checks []*domain.EndpointHealthCheck
|
|
for rows.Next() {
|
|
check, err := scanHealthCheck(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
checks = append(checks, check)
|
|
}
|
|
return checks, rows.Err()
|
|
}
|
|
|
|
// GetByEndpoint retrieves a health check by endpoint address.
|
|
func (r *HealthCheckRepository) GetByEndpoint(ctx context.Context, endpoint string) (*domain.EndpointHealthCheck, error) {
|
|
row := r.db.QueryRowContext(ctx, `
|
|
SELECT id, endpoint, certificate_id, network_scan_target_id,
|
|
expected_fingerprint, observed_fingerprint, status,
|
|
consecutive_failures, response_time_ms, tls_version, cipher_suite,
|
|
cert_subject, cert_issuer, cert_expiry,
|
|
last_checked_at, last_success_at, last_failure_at, last_transition_at,
|
|
failure_reason, degraded_threshold, down_threshold, check_interval_seconds,
|
|
enabled, acknowledged, acknowledged_by, acknowledged_at,
|
|
created_at, updated_at
|
|
FROM endpoint_health_checks
|
|
WHERE endpoint = $1`, endpoint)
|
|
check := &domain.EndpointHealthCheck{}
|
|
var status string
|
|
var certExpiry, lastCheckedAt, lastSuccessAt, lastFailureAt, lastTransitionAt, acknowledgedAt sql.NullTime
|
|
err := row.Scan(
|
|
&check.ID, &check.Endpoint, &check.CertificateID, &check.NetworkScanTargetID,
|
|
&check.ExpectedFingerprint, &check.ObservedFingerprint, &status,
|
|
&check.ConsecutiveFailures, &check.ResponseTimeMs, &check.TLSVersion, &check.CipherSuite,
|
|
&check.CertSubject, &check.CertIssuer, &certExpiry,
|
|
&lastCheckedAt, &lastSuccessAt, &lastFailureAt, &lastTransitionAt,
|
|
&check.FailureReason, &check.DegradedThreshold, &check.DownThreshold, &check.CheckIntervalSecs,
|
|
&check.Enabled, &check.Acknowledged, &check.AcknowledgedBy, &acknowledgedAt,
|
|
&check.CreatedAt, &check.UpdatedAt,
|
|
)
|
|
if err == sql.ErrNoRows {
|
|
return nil, fmt.Errorf("health check not found for endpoint: %w", repository.ErrNotFound)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get health check by endpoint: %w", err)
|
|
}
|
|
check.Status = domain.HealthStatus(status)
|
|
if certExpiry.Valid {
|
|
check.CertExpiry = &certExpiry.Time
|
|
}
|
|
if lastCheckedAt.Valid {
|
|
check.LastCheckedAt = &lastCheckedAt.Time
|
|
}
|
|
if lastSuccessAt.Valid {
|
|
check.LastSuccessAt = &lastSuccessAt.Time
|
|
}
|
|
if lastFailureAt.Valid {
|
|
check.LastFailureAt = &lastFailureAt.Time
|
|
}
|
|
if lastTransitionAt.Valid {
|
|
check.LastTransitionAt = &lastTransitionAt.Time
|
|
}
|
|
if acknowledgedAt.Valid {
|
|
check.AcknowledgedAt = &acknowledgedAt.Time
|
|
}
|
|
return check, nil
|
|
}
|
|
|
|
// RecordHistory records a single probe result in history.
|
|
func (r *HealthCheckRepository) RecordHistory(ctx context.Context, entry *domain.HealthHistoryEntry) error {
|
|
_, err := r.db.ExecContext(ctx, `
|
|
INSERT INTO endpoint_health_history (id, health_check_id, status, response_time_ms, fingerprint, failure_reason, checked_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
|
entry.ID, entry.HealthCheckID, entry.Status, entry.ResponseTimeMs, entry.Fingerprint, entry.FailureReason, entry.CheckedAt,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("record health check history: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetHistory retrieves recent probe history for a health check.
|
|
func (r *HealthCheckRepository) GetHistory(ctx context.Context, healthCheckID string, limit int) ([]*domain.HealthHistoryEntry, error) {
|
|
if limit <= 0 {
|
|
limit = 100
|
|
}
|
|
rows, err := r.db.QueryContext(ctx, `
|
|
SELECT id, health_check_id, status, response_time_ms, fingerprint, failure_reason, checked_at
|
|
FROM endpoint_health_history
|
|
WHERE health_check_id = $1
|
|
ORDER BY checked_at DESC
|
|
LIMIT $2`, healthCheckID, limit)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get health check history: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var entries []*domain.HealthHistoryEntry
|
|
for rows.Next() {
|
|
entry := &domain.HealthHistoryEntry{}
|
|
if err := rows.Scan(&entry.ID, &entry.HealthCheckID, &entry.Status, &entry.ResponseTimeMs, &entry.Fingerprint, &entry.FailureReason, &entry.CheckedAt); err != nil {
|
|
return nil, fmt.Errorf("scan health history entry: %w", err)
|
|
}
|
|
entries = append(entries, entry)
|
|
}
|
|
return entries, rows.Err()
|
|
}
|
|
|
|
// PurgeHistory deletes history entries older than the specified time.
|
|
func (r *HealthCheckRepository) PurgeHistory(ctx context.Context, olderThan time.Time) (int64, error) {
|
|
result, err := r.db.ExecContext(ctx, `DELETE FROM endpoint_health_history WHERE checked_at < $1`, olderThan)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("purge health check history: %w", err)
|
|
}
|
|
return result.RowsAffected()
|
|
}
|
|
|
|
// GetSummary returns aggregate counts by health status.
|
|
func (r *HealthCheckRepository) GetSummary(ctx context.Context) (*domain.HealthCheckSummary, error) {
|
|
rows, err := r.db.QueryContext(ctx, `SELECT status, COUNT(*) FROM endpoint_health_checks GROUP BY status`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get health check summary: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
summary := &domain.HealthCheckSummary{}
|
|
for rows.Next() {
|
|
var status string
|
|
var count int
|
|
if err := rows.Scan(&status, &count); err != nil {
|
|
return nil, fmt.Errorf("scan health check summary: %w", err)
|
|
}
|
|
switch domain.HealthStatus(status) {
|
|
case domain.HealthStatusHealthy:
|
|
summary.Healthy = count
|
|
case domain.HealthStatusDegraded:
|
|
summary.Degraded = count
|
|
case domain.HealthStatusDown:
|
|
summary.Down = count
|
|
case domain.HealthStatusCertMismatch:
|
|
summary.CertMismatch = count
|
|
case domain.HealthStatusUnknown:
|
|
summary.Unknown = count
|
|
}
|
|
summary.Total += count
|
|
}
|
|
return summary, rows.Err()
|
|
}
|
|
|
|
// scannable is an interface satisfied by both *sql.Row and *sql.Rows.
|
|
type scannable interface {
|
|
Scan(dest ...interface{}) error
|
|
}
|
|
|
|
// scanHealthCheck scans a health check from a row.
|
|
func scanHealthCheck(row scannable) (*domain.EndpointHealthCheck, error) {
|
|
check := &domain.EndpointHealthCheck{}
|
|
var status string
|
|
var certExpiry, lastCheckedAt, lastSuccessAt, lastFailureAt, lastTransitionAt, acknowledgedAt sql.NullTime
|
|
err := row.Scan(
|
|
&check.ID, &check.Endpoint, &check.CertificateID, &check.NetworkScanTargetID,
|
|
&check.ExpectedFingerprint, &check.ObservedFingerprint, &status,
|
|
&check.ConsecutiveFailures, &check.ResponseTimeMs, &check.TLSVersion, &check.CipherSuite,
|
|
&check.CertSubject, &check.CertIssuer, &certExpiry,
|
|
&lastCheckedAt, &lastSuccessAt, &lastFailureAt, &lastTransitionAt,
|
|
&check.FailureReason, &check.DegradedThreshold, &check.DownThreshold, &check.CheckIntervalSecs,
|
|
&check.Enabled, &check.Acknowledged, &check.AcknowledgedBy, &acknowledgedAt,
|
|
&check.CreatedAt, &check.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("scan health check: %w", err)
|
|
}
|
|
check.Status = domain.HealthStatus(status)
|
|
if certExpiry.Valid {
|
|
check.CertExpiry = &certExpiry.Time
|
|
}
|
|
if lastCheckedAt.Valid {
|
|
check.LastCheckedAt = &lastCheckedAt.Time
|
|
}
|
|
if lastSuccessAt.Valid {
|
|
check.LastSuccessAt = &lastSuccessAt.Time
|
|
}
|
|
if lastFailureAt.Valid {
|
|
check.LastFailureAt = &lastFailureAt.Time
|
|
}
|
|
if lastTransitionAt.Valid {
|
|
check.LastTransitionAt = &lastTransitionAt.Time
|
|
}
|
|
if acknowledgedAt.Valid {
|
|
check.AcknowledgedAt = &acknowledgedAt.Time
|
|
}
|
|
return check, nil
|
|
}
|