164 lines
4.8 KiB
Go
164 lines
4.8 KiB
Go
// SPDX-License-Identifier: MIT
|
|
|
|
package checker
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
|
)
|
|
|
|
type overallStatusRule struct{}
|
|
|
|
func (r *overallStatusRule) Name() string { return "dnsviz_overall_status" }
|
|
func (r *overallStatusRule) Description() string {
|
|
return "Reports the DNSViz status of the queried domain (SECURE, INSECURE, BOGUS, INDETERMINATE)."
|
|
}
|
|
func (r *overallStatusRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errState := loadData(ctx, obs, "dnsviz_overall_status")
|
|
if errState != nil {
|
|
return errState
|
|
}
|
|
leaf := data.Domain + "."
|
|
z, ok := data.Zones[leaf]
|
|
if !ok {
|
|
// Fall back to the most-specific zone DNSViz reported.
|
|
zones := orderedZones(data)
|
|
if len(zones) == 0 {
|
|
return []sdk.CheckState{{
|
|
Status: sdk.StatusUnknown,
|
|
Code: "dnsviz_overall_status",
|
|
Message: "DNSViz returned no zones for this domain",
|
|
}}
|
|
}
|
|
leaf = zones[0]
|
|
z = data.Zones[leaf]
|
|
}
|
|
st := sdk.CheckState{
|
|
Code: "dnsviz_overall_status",
|
|
Subject: leaf,
|
|
Status: statusFromGrok(z.Status),
|
|
Message: fmt.Sprintf("DNSViz status: %s", emptyAsUnknown(z.Status)),
|
|
Meta: map[string]any{
|
|
"status": z.Status,
|
|
"errors": len(z.Errors),
|
|
"warnings": len(z.Warnings),
|
|
},
|
|
}
|
|
return []sdk.CheckState{st}
|
|
}
|
|
|
|
// Subject is set to the zone name so each delegation level gets its own report block.
|
|
type perZoneStatusRule struct{}
|
|
|
|
func (r *perZoneStatusRule) Name() string { return "dnsviz_per_zone_status" }
|
|
func (r *perZoneStatusRule) Description() string {
|
|
return "Reports the DNSViz status of every zone in the chain (root, TLD, intermediates, leaf)."
|
|
}
|
|
func (r *perZoneStatusRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errState := loadData(ctx, obs, "dnsviz_per_zone_status")
|
|
if errState != nil {
|
|
return errState
|
|
}
|
|
zones := orderedZones(data)
|
|
if len(zones) == 0 {
|
|
return []sdk.CheckState{{
|
|
Status: sdk.StatusUnknown,
|
|
Code: "dnsviz_per_zone_status",
|
|
Message: "DNSViz returned no zones for this domain",
|
|
}}
|
|
}
|
|
out := make([]sdk.CheckState, 0, len(zones))
|
|
for _, name := range zones {
|
|
z := data.Zones[name]
|
|
out = append(out, sdk.CheckState{
|
|
Code: "dnsviz_per_zone_status",
|
|
Subject: name,
|
|
Status: statusFromGrok(z.Status),
|
|
Message: fmt.Sprintf("%s: errors=%d warnings=%d", emptyAsUnknown(z.Status), len(z.Errors), len(z.Warnings)),
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// One state per (zone, finding) pair so the UI can show a precise list.
|
|
type zoneErrorsRule struct{}
|
|
|
|
func (r *zoneErrorsRule) Name() string { return "dnsviz_zone_errors" }
|
|
func (r *zoneErrorsRule) Description() string {
|
|
return "Surfaces every error reported by DNSViz, scoped to the zone where it was found."
|
|
}
|
|
func (r *zoneErrorsRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errState := loadData(ctx, obs, "dnsviz_zone_errors")
|
|
if errState != nil {
|
|
return errState
|
|
}
|
|
return zoneFindingStates(data, "dnsviz_zone_errors", sdk.StatusCrit, "errors", func(z ZoneAnalysis) []Finding { return z.Errors })
|
|
}
|
|
|
|
type zoneWarningsRule struct{}
|
|
|
|
func (r *zoneWarningsRule) Name() string { return "dnsviz_zone_warnings" }
|
|
func (r *zoneWarningsRule) Description() string {
|
|
return "Surfaces every warning reported by DNSViz, scoped to the zone where it was found."
|
|
}
|
|
func (r *zoneWarningsRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errState := loadData(ctx, obs, "dnsviz_zone_warnings")
|
|
if errState != nil {
|
|
return errState
|
|
}
|
|
return zoneFindingStates(data, "dnsviz_zone_warnings", sdk.StatusWarn, "warnings", func(z ZoneAnalysis) []Finding { return z.Warnings })
|
|
}
|
|
|
|
// zoneFindingStates emits a single OK state when nothing matches so the rule outcome is always observable.
|
|
func zoneFindingStates(data *DNSVizData, ruleCode string, status sdk.Status, kindLabel string, pick func(ZoneAnalysis) []Finding) []sdk.CheckState {
|
|
var out []sdk.CheckState
|
|
for _, name := range orderedZones(data) {
|
|
for _, f := range pick(data.Zones[name]) {
|
|
out = append(out, sdk.CheckState{
|
|
Status: status,
|
|
Code: nonEmpty(f.Code, ruleCode),
|
|
Subject: name,
|
|
Message: f.Description,
|
|
Meta: findingMeta(f),
|
|
})
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return []sdk.CheckState{{
|
|
Status: sdk.StatusOK,
|
|
Code: ruleCode,
|
|
Message: fmt.Sprintf("DNSViz reported no %s in any zone", kindLabel),
|
|
}}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func emptyAsUnknown(s string) string {
|
|
if s == "" {
|
|
return "UNKNOWN"
|
|
}
|
|
return s
|
|
}
|
|
|
|
func nonEmpty(a, b string) string {
|
|
if a != "" {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func findingMeta(f Finding) map[string]any {
|
|
m := map[string]any{}
|
|
if f.Code != "" {
|
|
m["code"] = f.Code
|
|
}
|
|
if len(f.Servers) > 0 {
|
|
m["servers"] = f.Servers
|
|
}
|
|
if len(m) == 0 {
|
|
return nil
|
|
}
|
|
return m
|
|
}
|