Files
LinkPort/internal/alerts/conditions.go
T

120 lines
4.3 KiB
Go

package alerts
import (
"context"
"linkport/internal/model"
"linkport/internal/store"
"time"
)
type conditionSink func(store.Alert, bool) error
// conditionEvaluator owns rule state; persistence and notification stay with Engine.
// Engine serializes evaluations so authentication windows retain their baseline.
type conditionEvaluator struct {
source Source
samples map[string][]sample
}
type sample struct {
at time.Time
count int64
}
func (r *conditionEvaluator) evaluate(ctx context.Context, c store.AlertSettings, ps []model.Proxy, ts []model.Target, now time.Time, put conditionSink) error {
var err error
targets := map[string]bool{}
for _, t := range ts {
targets[t.ID] = true
}
_, health := r.source.HealthSnapshot()
for _, h := range health {
if !targets[h.TargetID] {
continue
}
a := store.Alert{Key: "target_unavailable:" + h.TargetID + ":" + h.AccountID, Kind: "target_unavailable", ResourceID: h.TargetID, AccountID: h.AccountID, Severity: "critical", Message: "Target health check failed"}
if err = put(a, !h.Healthy); err != nil {
return err
}
}
for _, p := range ps {
if err = ctx.Err(); err != nil {
return err
}
count := r.source.Metrics(p.ID).AuthenticationFailed
samples := r.samples[p.ID]
if len(samples) > 0 && count < samples[len(samples)-1].count {
samples = nil
}
samples = append(samples, sample{now, count})
cut := now.Add(-time.Duration(c.AuthFailureWindowSeconds) * time.Second)
// Keep the most recent baseline at or before the window boundary.
for len(samples) > 1 && !samples[1].at.After(cut) {
samples = samples[1:]
}
r.samples[p.ID] = samples
delta := count - samples[0].count
if err = put(store.Alert{Key: "auth_failure_spike:" + p.ID, Kind: "auth_failure_spike", ResourceID: p.ID, Severity: "warning", Message: "Gateway authentication failures exceeded the configured window threshold"}, delta >= int64(c.AuthFailureThreshold)); err != nil {
return err
}
for _, a := range p.GatewayAccounts {
if err = credential(put, "gateway", p.ID, a.ID, a.ExpiresAt, a.RotationReminderDays, a.Disabled, now); err != nil {
return err
}
}
}
for _, t := range ts {
for _, a := range t.Accounts {
if err = credential(put, "target", t.ID, a.ID, a.ExpiresAt, a.RotationReminderDays, false, now); err != nil {
return err
}
}
}
if certs, ok := r.source.(interface{ CertificateExpirations() map[string]time.Time }); ok {
for id, expiry := range certs.CertificateExpirations() {
a := store.Alert{Key: "certificate_expiry:" + id, Kind: "certificate_expiring", ResourceID: id, Severity: "warning", Message: "TLS certificate expires soon"}
if !expiry.After(now) {
a.Kind = "certificate_expired"
a.Severity = "critical"
a.Message = "TLS certificate has expired"
}
if err = put(a, !expiry.After(now.Add(time.Duration(c.CertificateReminderDays)*24*time.Hour))); err != nil {
return err
}
}
}
return nil
}
func evaluateAssetCertificates(assets []model.Asset, c store.AlertSettings, now time.Time, put conditionSink) error {
var err error
for _, asset := range assets {
summary := asset.Summary(now)
expiry, parseErr := time.Parse(time.RFC3339, summary.ExpiresAt)
if parseErr != nil {
continue
}
a := store.Alert{Key: "asset_certificate_expiry:" + asset.ID, Kind: "certificate_expiring", ResourceID: asset.ID, Severity: "warning", Message: "Asset certificate expires soon; upload a renewed certificate"}
if !expiry.After(now) {
a.Kind = "certificate_expired"
a.Severity = "critical"
a.Message = "Asset certificate has expired; upload a renewed certificate"
}
if err = put(a, !expiry.After(now.Add(time.Duration(c.CertificateReminderDays)*24*time.Hour))); err != nil {
return err
}
}
return nil
}
func credential(put func(store.Alert, bool) error, scope, id, account, expiry string, days int, disabled bool, now time.Time) error {
due, expired := model.CredentialDue(expiry, days, now)
a := store.Alert{Key: "credential:" + scope + ":" + id + ":" + account, Kind: "credential_expiring", ResourceID: id, AccountID: account, Severity: "warning", Message: "Account credential expires soon; rotate it before expiry"}
if expired {
a.Kind = "credential_expired"
a.Severity = "critical"
a.Message = "Account credential has expired"
}
return put(a, due && !disabled)
}