185 lines
5.3 KiB
Go
185 lines
5.3 KiB
Go
// SPDX-License-Identifier: MIT
|
|
|
|
package checker
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
|
)
|
|
|
|
// overallStatusRule reports on the leaf zone's DNSViz status. A SECURE leaf
|
|
// means the entire chain validates from the root; BOGUS means at least one
|
|
// link of the chain is broken.
|
|
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}
|
|
}
|
|
|
|
// perZoneStatusRule emits one CheckState per zone in the chain. This is what
|
|
// powers the "every authoritative/parent in a dedicated block" requirement
|
|
// of the report: each entry has Subject set to the zone name.
|
|
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
|
|
}
|
|
|
|
// zoneErrorsRule turns every DNSViz "error" entry into a Crit CheckState.
|
|
// 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
|
|
}
|
|
var out []sdk.CheckState
|
|
for _, name := range orderedZones(data) {
|
|
for _, f := range data.Zones[name].Errors {
|
|
out = append(out, sdk.CheckState{
|
|
Status: sdk.StatusCrit,
|
|
Code: nonEmpty(f.Code, "dnsviz_zone_errors"),
|
|
Subject: name,
|
|
Message: f.Description,
|
|
Meta: findingMeta(f),
|
|
})
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return []sdk.CheckState{{
|
|
Status: sdk.StatusOK,
|
|
Code: "dnsviz_zone_errors",
|
|
Message: "DNSViz reported no errors in any zone",
|
|
}}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// zoneWarningsRule mirrors zoneErrorsRule for warnings (StatusWarn).
|
|
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
|
|
}
|
|
var out []sdk.CheckState
|
|
for _, name := range orderedZones(data) {
|
|
for _, f := range data.Zones[name].Warnings {
|
|
out = append(out, sdk.CheckState{
|
|
Status: sdk.StatusWarn,
|
|
Code: nonEmpty(f.Code, "dnsviz_zone_warnings"),
|
|
Subject: name,
|
|
Message: f.Description,
|
|
Meta: findingMeta(f),
|
|
})
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return []sdk.CheckState{{
|
|
Status: sdk.StatusOK,
|
|
Code: "dnsviz_zone_warnings",
|
|
Message: "DNSViz reported no warnings in any zone",
|
|
}}
|
|
}
|
|
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
|
|
}
|