checker-ns-restrictions/checker/rule.go

136 lines
4 KiB
Go

package checker
import (
"context"
"fmt"
"strings"
sdk "git.happydns.org/checker-sdk-go/checker"
)
// Rules returns one rule per individual NS security check. Every rule
// reads the same shared observation produced by Collect and only looks
// at its own check entry, so a single network round trip feeds all rules.
func Rules() []sdk.CheckRule {
return []sdk.CheckRule{
&singleCheckRule{
ruleName: "ns_axfr_refused",
description: "Verifies that AXFR zone transfers are refused by every authoritative nameserver",
checkName: checkNameAXFR,
failStatus: sdk.StatusCrit,
code: "ns_axfr",
},
&singleCheckRule{
ruleName: "ns_ixfr_refused",
description: "Verifies that IXFR zone transfers are refused by every authoritative nameserver",
checkName: checkNameIXFR,
failStatus: sdk.StatusWarn,
code: "ns_ixfr",
},
&singleCheckRule{
ruleName: "ns_no_recursion",
description: "Verifies that authoritative nameservers do not advertise recursion (RA bit unset)",
checkName: checkNameNoRecursion,
failStatus: sdk.StatusWarn,
code: "ns_recursion",
},
&singleCheckRule{
ruleName: "ns_any_handled",
description: "Verifies that ANY queries are handled per RFC 8482 (HINFO or minimal answer)",
checkName: checkNameANYHandled,
failStatus: sdk.StatusWarn,
code: "ns_any",
},
&singleCheckRule{
ruleName: "ns_is_authoritative",
description: "Verifies that nameservers answer authoritatively (AA bit set) for the zone",
checkName: checkNameIsAuthoritative,
failStatus: sdk.StatusInfo,
code: "ns_authoritative",
},
}
}
// singleCheckRule evaluates one named check across all servers in the
// shared NSRestrictionsReport observation.
type singleCheckRule struct {
ruleName string
description string
checkName string
failStatus sdk.Status
code string
}
func (r *singleCheckRule) Name() string { return r.ruleName }
func (r *singleCheckRule) Description() string { return r.description }
func (r *singleCheckRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) sdk.CheckState {
var report NSRestrictionsReport
if err := obs.Get(ctx, ObservationKeyNSRestrictions, &report); err != nil {
return sdk.CheckState{
Status: sdk.StatusError,
Message: fmt.Sprintf("Failed to get NS restrictions data: %v", err),
Code: r.code + "_error",
}
}
status := sdk.StatusOK
var summaryParts []string
failingServers := make([]map[string]string, 0)
for _, srv := range report.Servers {
item, found := findCheck(srv.Checks, r.checkName)
if !found {
// The collect step did not run this check on this server
// (e.g. IPv6 unreachable, DNS resolution failure). Surface
// the reason from whichever entry the server does have.
if len(srv.Checks) > 0 {
summaryParts = append(summaryParts, fmt.Sprintf("%s: skipped (%s)", serverLabel(srv), srv.Checks[0].Detail))
} else {
summaryParts = append(summaryParts, fmt.Sprintf("%s: skipped", serverLabel(srv)))
}
continue
}
if item.OK {
summaryParts = append(summaryParts, fmt.Sprintf("%s: OK", serverLabel(srv)))
continue
}
if status < r.failStatus {
status = r.failStatus
}
summaryParts = append(summaryParts, fmt.Sprintf("%s: FAIL (%s)", serverLabel(srv), item.Detail))
failingServers = append(failingServers, map[string]string{
"name": srv.Name,
"address": srv.Address,
"detail": item.Detail,
})
}
return sdk.CheckState{
Status: status,
Message: strings.Join(summaryParts, " | "),
Code: r.code + "_result",
Meta: map[string]any{
"check": r.checkName,
"failing_servers": failingServers,
},
}
}
func findCheck(items []NSCheckItem, name string) (NSCheckItem, bool) {
for _, it := range items {
if it.Name == name {
return it, true
}
}
return NSCheckItem{}, false
}
func serverLabel(srv NSServerResult) string {
if srv.Address == "" {
return srv.Name
}
return fmt.Sprintf("%s (%s)", srv.Name, srv.Address)
}