Split monolithic rule into per-test rules, collect gathers facts only
This commit is contained in:
parent
5b71e85f49
commit
4177fcdc7b
14 changed files with 758 additions and 259 deletions
175
checker/rule.go
175
checker/rule.go
|
|
@ -8,140 +8,81 @@ import (
|
|||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
// Rule returns the rule that aggregates per-endpoint TLS probe outcomes into
|
||||
// a single status for this checker run.
|
||||
func Rule() sdk.CheckRule {
|
||||
return &tlsRule{}
|
||||
// Rules returns the full list of CheckRules exposed by the TLS checker.
|
||||
// Each rule covers a single concern (reachability, handshake, chain, hostname,
|
||||
// expiry, TLS version, STARTTLS advertisement, cipher suite, …) so the UI can
|
||||
// surface a passing-list rather than a single aggregated code.
|
||||
func Rules() []sdk.CheckRule {
|
||||
return []sdk.CheckRule{
|
||||
&endpointsDiscoveredRule{},
|
||||
&reachabilityRule{},
|
||||
&tlsHandshakeRule{},
|
||||
&starttlsAdvertisedRule{},
|
||||
&starttlsSupportedRule{},
|
||||
&peerCertificateRule{},
|
||||
&chainValidityRule{},
|
||||
&hostnameMatchRule{},
|
||||
&expiryRule{},
|
||||
&tlsVersionRule{},
|
||||
&cipherSuiteRule{},
|
||||
}
|
||||
}
|
||||
|
||||
type tlsRule struct{}
|
||||
|
||||
func (r *tlsRule) Name() string { return "tls_posture" }
|
||||
|
||||
func (r *tlsRule) Description() string {
|
||||
return "Summarises TLS handshake, certificate validity, hostname match and expiry across all probed endpoints"
|
||||
}
|
||||
|
||||
func (r *tlsRule) ValidateOptions(opts sdk.CheckerOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *tlsRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
||||
// loadData fetches the TLS observation. On error, returns a single error
|
||||
// state the caller should emit.
|
||||
func loadData(ctx context.Context, obs sdk.ObservationGetter) (*TLSData, *sdk.CheckState) {
|
||||
var data TLSData
|
||||
if err := obs.Get(ctx, ObservationKeyTLSProbes, &data); err != nil {
|
||||
return []sdk.CheckState{{
|
||||
return nil, &sdk.CheckState{
|
||||
Status: sdk.StatusError,
|
||||
Message: fmt.Sprintf("Failed to read tls_probes: %v", err),
|
||||
Code: "tls_observation_error",
|
||||
}}
|
||||
}
|
||||
|
||||
// Steady state when no producer has published entries for this target
|
||||
// yet (or when the last producer run cleared them). Report Unknown so
|
||||
// we don't flap red during the eventual-consistency window between a
|
||||
// fresh enrollment and the first producer cycle.
|
||||
if len(data.Probes) == 0 {
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusUnknown,
|
||||
Message: "No TLS endpoints have been discovered for this target yet",
|
||||
Code: "tls_no_endpoints",
|
||||
}}
|
||||
Message: fmt.Sprintf("failed to load tls_probes observation: %v", err),
|
||||
Code: "tls.observation_error",
|
||||
}
|
||||
}
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
// sortedRefs returns the probe refs in deterministic order. Rules iterate
|
||||
// this sorted list so CheckState output is stable.
|
||||
func sortedRefs(data *TLSData) []string {
|
||||
refs := make([]string, 0, len(data.Probes))
|
||||
for ref := range data.Probes {
|
||||
refs = append(refs, ref)
|
||||
}
|
||||
sort.Strings(refs)
|
||||
|
||||
out := make([]sdk.CheckState, 0, len(refs))
|
||||
for _, ref := range refs {
|
||||
p := data.Probes[ref]
|
||||
out = append(out, evaluateProbe(p))
|
||||
}
|
||||
return out
|
||||
return refs
|
||||
}
|
||||
|
||||
// evaluateProbe distills a single TLSProbe into a CheckState. Subject is the
|
||||
// probed endpoint so the host can correlate states across runs and surface
|
||||
// them per-target in the UI. Message describes the finding only -- the UI
|
||||
// renders Subject separately.
|
||||
func evaluateProbe(p TLSProbe) sdk.CheckState {
|
||||
subject := fmt.Sprintf("%s://%s", p.Type, p.Endpoint)
|
||||
meta := map[string]any{
|
||||
"type": p.Type,
|
||||
"host": p.Host,
|
||||
"port": p.Port,
|
||||
"sni": p.SNI,
|
||||
"issues": len(p.Issues),
|
||||
// subjectOf formats the UI-facing subject for a single probe.
|
||||
func subjectOf(p TLSProbe) string {
|
||||
return fmt.Sprintf("%s://%s", p.Type, p.Endpoint)
|
||||
}
|
||||
|
||||
// metaOf returns a compact meta map to attach to a CheckState.
|
||||
func metaOf(p TLSProbe) map[string]any {
|
||||
m := map[string]any{
|
||||
"type": p.Type,
|
||||
"host": p.Host,
|
||||
"port": p.Port,
|
||||
"sni": p.SNI,
|
||||
}
|
||||
if p.TLSVersion != "" {
|
||||
meta["tls_version"] = p.TLSVersion
|
||||
}
|
||||
if !p.NotAfter.IsZero() {
|
||||
meta["not_after"] = p.NotAfter
|
||||
}
|
||||
|
||||
worst, critMsg, warnMsg := summarize(p.Issues)
|
||||
switch worst {
|
||||
case SeverityCrit:
|
||||
return sdk.CheckState{
|
||||
Status: sdk.StatusCrit,
|
||||
Message: critMsg,
|
||||
Code: "tls_critical",
|
||||
Subject: subject,
|
||||
Meta: meta,
|
||||
}
|
||||
case SeverityWarn:
|
||||
return sdk.CheckState{
|
||||
Status: sdk.StatusWarn,
|
||||
Message: warnMsg,
|
||||
Code: "tls_warning",
|
||||
Subject: subject,
|
||||
Meta: meta,
|
||||
}
|
||||
default:
|
||||
msg := "TLS endpoint OK"
|
||||
if p.TLSVersion != "" {
|
||||
msg = fmt.Sprintf("TLS endpoint OK (%s)", p.TLSVersion)
|
||||
}
|
||||
return sdk.CheckState{
|
||||
Status: sdk.StatusOK,
|
||||
Message: msg,
|
||||
Code: "tls_ok",
|
||||
Subject: subject,
|
||||
Meta: meta,
|
||||
}
|
||||
m["tls_version"] = p.TLSVersion
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// summarize walks the issues once and returns (worst severity, first
|
||||
// critical message, first warning message). Picking the messages during the
|
||||
// same pass avoids a second iteration in the caller.
|
||||
func summarize(issues []Issue) (worst, firstCrit, firstWarn string) {
|
||||
for _, is := range issues {
|
||||
msg := is.Message
|
||||
if msg == "" {
|
||||
msg = is.Code
|
||||
}
|
||||
switch is.Severity {
|
||||
case SeverityCrit:
|
||||
worst = SeverityCrit
|
||||
if firstCrit == "" {
|
||||
firstCrit = msg
|
||||
}
|
||||
case SeverityWarn:
|
||||
if worst == "" || worst == SeverityInfo {
|
||||
worst = SeverityWarn
|
||||
}
|
||||
if firstWarn == "" {
|
||||
firstWarn = msg
|
||||
}
|
||||
case SeverityInfo:
|
||||
if worst == "" {
|
||||
worst = SeverityInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
// passState / infoState / unknownState helpers.
|
||||
func passState(code, message string) sdk.CheckState {
|
||||
return sdk.CheckState{Status: sdk.StatusOK, Code: code, Message: message}
|
||||
}
|
||||
func unknownState(code, message string) sdk.CheckState {
|
||||
return sdk.CheckState{Status: sdk.StatusUnknown, Code: code, Message: message}
|
||||
}
|
||||
|
||||
// emptyCaseState returns a single state describing "no probes to evaluate".
|
||||
// Rules call this when len(data.Probes) == 0 to avoid returning an empty
|
||||
// slice (see CheckRule.Evaluate contract).
|
||||
func emptyCaseState(code string) sdk.CheckState {
|
||||
return unknownState(code, "No TLS endpoints have been discovered for this target yet.")
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue