checker-dnssec/checker/rules_enumeration_test.go

188 lines
5 KiB
Go

package checker
import (
"context"
"encoding/json"
"strings"
"testing"
sdk "git.happydns.org/checker-sdk-go/checker"
)
// fakeObs round-trips through JSON like the production read path so tests
// catch any tag drift between DNSSECData fields and rule expectations.
type fakeObs struct{ data *DNSSECData }
func (f fakeObs) Get(_ context.Context, _ sdk.ObservationKey, dest any) error {
if f.data == nil {
return nil
}
raw, err := json.Marshal(f.data)
if err != nil {
return err
}
return json.Unmarshal(raw, dest)
}
func (fakeObs) GetRelated(_ context.Context, _ sdk.ObservationKey) ([]sdk.RelatedObservation, error) {
return nil, nil
}
func run(r sdk.CheckRule, data *DNSSECData, opts sdk.CheckerOptions) []sdk.CheckState {
return r.Evaluate(context.Background(), fakeObs{data: data}, opts)
}
func signedZone(denial DenialKind, p *NSEC3ParamObservation) *DNSSECData {
return &DNSSECData{
Domain: "example.com",
Servers: map[string]PerServerView{
"ns1.example.com.:53": {
Server: "ns1.example.com.:53",
DNSKEYs: []DNSKEYRecord{{Flags: 257, Algorithm: 13, KeyTag: 12345, IsKSK: true}},
NSEC3PARAM: p,
DenialKind: denial,
},
},
}
}
func wantStatus(t *testing.T, states []sdk.CheckState, want sdk.Status) {
t.Helper()
if len(states) == 0 {
t.Fatalf("no states returned")
}
if states[0].Status != want {
t.Fatalf("status = %v, want %v: %+v", states[0].Status, want, states[0])
}
}
func TestDenialUsesNSEC3(t *testing.T) {
cases := []struct {
name string
data *DNSSECData
want sdk.Status
}{
{
name: "NSEC zone is walkable -> WARN",
data: signedZone(DenialNSEC, nil),
want: sdk.StatusWarn,
},
{
name: "NSEC3 zone -> OK",
data: signedZone(DenialNSEC3, &NSEC3ParamObservation{Iterations: 0}),
want: sdk.StatusOK,
},
{
name: "OPT-OUT zone -> OK",
data: signedZone(DenialOptOut, &NSEC3ParamObservation{Iterations: 0, Flags: 1}),
want: sdk.StatusOK,
},
{
name: "Unsigned zone -> INFO",
data: &DNSSECData{Domain: "x", Servers: map[string]PerServerView{
"ns1:53": {Server: "ns1:53", DenialKind: DenialNone},
}},
want: sdk.StatusInfo,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
wantStatus(t, run(denialUsesNSEC3Rule{}, tc.data, nil), tc.want)
})
}
}
func TestNSEC3Iterations(t *testing.T) {
cases := []struct {
name string
iter uint16
opts sdk.CheckerOptions
want sdk.Status
}{
{"iter=0 -> OK", 0, nil, sdk.StatusOK},
{"iter=1 default ceiling 0 -> WARN", 1, nil, sdk.StatusWarn},
{"iter=10 default ceiling 0 -> WARN", 10, nil, sdk.StatusWarn},
{"iter=10 ceiling 100 -> OK", 10, sdk.CheckerOptions{"nsec3IterationsMax": float64(100)}, sdk.StatusOK},
{"iter=10 severity=crit -> CRIT", 10, sdk.CheckerOptions{"nsec3IterationsSeverity": "crit"}, sdk.StatusCrit},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
data := signedZone(DenialNSEC3, &NSEC3ParamObservation{Iterations: tc.iter})
wantStatus(t, run(nsec3IterationsRule{}, data, tc.opts), tc.want)
})
}
}
func TestNSEC3SaltEmpty(t *testing.T) {
cases := []struct {
name string
saltLength uint8
want sdk.Status
}{
{"empty salt -> OK", 0, sdk.StatusOK},
{"non-empty salt -> WARN", 8, sdk.StatusWarn},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
data := signedZone(DenialNSEC3, &NSEC3ParamObservation{
Iterations: 0, SaltLength: tc.saltLength, Salt: strings.Repeat("ab", int(tc.saltLength)),
})
wantStatus(t, run(nsec3SaltEmptyRule{}, data, nil), tc.want)
})
}
}
func TestNSEC3OptOut(t *testing.T) {
cases := []struct {
name string
flags uint8
want sdk.Status
}{
{"OPT-OUT off -> OK", 0, sdk.StatusOK},
{"OPT-OUT on -> INFO", 1, sdk.StatusInfo},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
data := signedZone(DenialNSEC3, &NSEC3ParamObservation{Iterations: 0, Flags: tc.flags})
wantStatus(t, run(nsec3OptOutRule{}, data, nil), tc.want)
})
}
}
func TestDenialConsistent(t *testing.T) {
consistent := &DNSSECData{
Domain: "x",
Servers: map[string]PerServerView{
"ns1:53": {Server: "ns1:53", DenialKind: DenialNSEC3},
"ns2:53": {Server: "ns2:53", DenialKind: DenialNSEC3},
},
}
wantStatus(t, run(denialConsistentRule{}, consistent, nil), sdk.StatusOK)
drifting := &DNSSECData{
Domain: "x",
Servers: map[string]PerServerView{
"ns1:53": {Server: "ns1:53", DenialKind: DenialNSEC},
"ns2:53": {Server: "ns2:53", DenialKind: DenialNSEC3},
},
}
wantStatus(t, run(denialConsistentRule{}, drifting, nil), sdk.StatusWarn)
}
func TestRoundTripJSON(t *testing.T) {
d := signedZone(DenialNSEC3, &NSEC3ParamObservation{Iterations: 0, SaltLength: 0})
raw, err := json.Marshal(d)
if err != nil {
t.Fatal(err)
}
var back DNSSECData
if err := json.Unmarshal(raw, &back); err != nil {
t.Fatal(err)
}
if back.Domain != d.Domain {
t.Fatalf("domain round-trip lost: %q vs %q", back.Domain, d.Domain)
}
if got := back.Servers["ns1.example.com.:53"].DenialKind; got != DenialNSEC3 {
t.Fatalf("denial round-trip lost: %v", got)
}
}