120 lines
4.3 KiB
Go
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)
|
|
}
|