checker: split monolithic rule into per-concern rules
This commit is contained in:
parent
d9a92ad576
commit
e8b38fac59
18 changed files with 1159 additions and 308 deletions
167
checker/rule.go
167
checker/rule.go
|
|
@ -7,135 +7,74 @@ import (
|
|||
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.
|
||||
// Rules returns one CheckRule per individual NS security concern. Every rule
|
||||
// reads the same shared observation produced by Collect and only looks at
|
||||
// its own facet of the raw probe data, so a single network round trip feeds
|
||||
// every rule.
|
||||
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",
|
||||
},
|
||||
&resolutionRule{},
|
||||
&axfrRule{},
|
||||
&ixfrRule{},
|
||||
&noRecursionRule{},
|
||||
&anyRFC8482Rule{},
|
||||
&authoritativeRule{},
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// loadReport fetches the shared NS observation. On error it returns a
|
||||
// CheckState the caller should emit verbatim to short-circuit its rule.
|
||||
func loadReport(ctx context.Context, obs sdk.ObservationGetter, errCode string) (*NSRestrictionsReport, *sdk.CheckState) {
|
||||
var report NSRestrictionsReport
|
||||
if err := obs.Get(ctx, ObservationKeyNSRestrictions, &report); err != nil {
|
||||
return []sdk.CheckState{{
|
||||
return nil, &sdk.CheckState{
|
||||
Status: sdk.StatusError,
|
||||
Message: fmt.Sprintf("Failed to get NS restrictions data: %v", err),
|
||||
Code: r.code + "_error",
|
||||
}}
|
||||
Code: errCode,
|
||||
}
|
||||
}
|
||||
|
||||
out := make([]sdk.CheckState, 0, len(report.Servers))
|
||||
for _, srv := range report.Servers {
|
||||
meta := map[string]any{
|
||||
"check": r.checkName,
|
||||
"name": srv.Name,
|
||||
"address": srv.Address,
|
||||
}
|
||||
|
||||
item, found := findCheck(srv.Checks, r.checkName)
|
||||
if !found {
|
||||
message := "check not performed"
|
||||
if len(srv.Checks) > 0 {
|
||||
message = fmt.Sprintf("skipped: %s", srv.Checks[0].Detail)
|
||||
}
|
||||
out = append(out, sdk.CheckState{
|
||||
Status: sdk.StatusUnknown,
|
||||
Message: message,
|
||||
Code: r.code + "_skipped",
|
||||
Subject: serverLabel(srv),
|
||||
Meta: meta,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
state := sdk.CheckState{
|
||||
Code: r.code + "_result",
|
||||
Subject: serverLabel(srv),
|
||||
Meta: meta,
|
||||
Message: item.Detail,
|
||||
}
|
||||
if item.OK {
|
||||
state.Status = sdk.StatusOK
|
||||
if state.Message == "" {
|
||||
state.Message = "OK"
|
||||
}
|
||||
} else {
|
||||
state.Status = r.failStatus
|
||||
}
|
||||
out = append(out, state)
|
||||
}
|
||||
|
||||
if len(out) == 0 {
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusUnknown,
|
||||
Message: "no nameserver to evaluate",
|
||||
Code: r.code + "_result",
|
||||
}}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func findCheck(items []NSCheckItem, name string) (NSCheckItem, bool) {
|
||||
for _, it := range items {
|
||||
if it.Name == name {
|
||||
return it, true
|
||||
}
|
||||
}
|
||||
return NSCheckItem{}, false
|
||||
return &report, nil
|
||||
}
|
||||
|
||||
// serverLabel returns a human-friendly subject for a given server result.
|
||||
func serverLabel(srv NSServerResult) string {
|
||||
if srv.Address == "" {
|
||||
return srv.Name
|
||||
}
|
||||
return fmt.Sprintf("%s (%s)", srv.Name, srv.Address)
|
||||
}
|
||||
|
||||
// serverMeta returns the per-server meta blob attached to every state a
|
||||
// rule produces.
|
||||
func serverMeta(srv NSServerResult) map[string]any {
|
||||
return map[string]any{
|
||||
"name": srv.Name,
|
||||
"address": srv.Address,
|
||||
}
|
||||
}
|
||||
|
||||
// probedServers returns only the servers that were actually probed
|
||||
// (i.e. DNS-resolved and not skipped). Rules that need to iterate over
|
||||
// probe results should call this helper to transparently skip the
|
||||
// resolution-error and address-skipped rows, which are the concern of
|
||||
// the dedicated resolutionRule.
|
||||
func probedServers(report *NSRestrictionsReport) []NSServerResult {
|
||||
out := make([]NSServerResult, 0, len(report.Servers))
|
||||
for _, s := range report.Servers {
|
||||
if s.ResolutionError != "" || s.AddressSkipped {
|
||||
continue
|
||||
}
|
||||
out = append(out, s)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// noProbesState returns the default state emitted when a rule has nothing to
|
||||
// evaluate (no server was successfully probed).
|
||||
func noProbesState(code string) sdk.CheckState {
|
||||
return sdk.CheckState{
|
||||
Status: sdk.StatusUnknown,
|
||||
Message: "no nameserver could be probed",
|
||||
Code: code,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue