246 lines
7.5 KiB
Go
246 lines
7.5 KiB
Go
package checker
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/miekg/dns"
|
|
|
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
|
)
|
|
|
|
// orphanService builds a fake "svcs.Orphan" service body whose embedded RR
|
|
// header matches the given (rrtype, owner). Used by every test below to
|
|
// avoid duplicating the JSON shape.
|
|
func orphanService(rrtype uint16, owner string) rawService {
|
|
body, _ := json.Marshal(map[string]any{
|
|
"record": map[string]any{
|
|
"Hdr": map[string]any{
|
|
"Name": owner,
|
|
"Rrtype": rrtype,
|
|
"Class": uint16(1),
|
|
"Ttl": uint32(3600),
|
|
},
|
|
},
|
|
})
|
|
return rawService{
|
|
Type: "svcs.Orphan",
|
|
Domain: owner,
|
|
Service: body,
|
|
}
|
|
}
|
|
|
|
// modernService builds a non-orphan service body with no Hdr field, like
|
|
// what a real svcs.MX or svcs.A would marshal. Used to assert the scanner
|
|
// silently ignores services it cannot inspect.
|
|
func modernService(svcType string) rawService {
|
|
body, _ := json.Marshal(map[string]any{"preference": 10, "target": "mail.example.com."})
|
|
return rawService{
|
|
Type: svcType,
|
|
Domain: "example.com.",
|
|
Service: body,
|
|
}
|
|
}
|
|
|
|
func runCollect(t *testing.T, zone *rawZone) *LegacyData {
|
|
t.Helper()
|
|
raw, err := json.Marshal(zone)
|
|
if err != nil {
|
|
t.Fatalf("marshal zone: %v", err)
|
|
}
|
|
var jsonZone map[string]any
|
|
if err := json.Unmarshal(raw, &jsonZone); err != nil {
|
|
t.Fatalf("unmarshal zone: %v", err)
|
|
}
|
|
|
|
p := &legacyProvider{}
|
|
out, err := p.Collect(context.Background(), sdk.CheckerOptions{"zone": jsonZone})
|
|
if err != nil {
|
|
t.Fatalf("Collect: %v", err)
|
|
}
|
|
data, ok := out.(*LegacyData)
|
|
if !ok {
|
|
t.Fatalf("Collect returned %T, want *LegacyData", out)
|
|
}
|
|
return data
|
|
}
|
|
|
|
func TestCollect_CleanZone(t *testing.T) {
|
|
z := &rawZone{
|
|
Services: map[string][]rawService{
|
|
"": {modernService("svcs.A"), modernService("svcs.MX")},
|
|
"www": {modernService("svcs.CNAME")},
|
|
"mail": {modernService("svcs.A")},
|
|
},
|
|
}
|
|
data := runCollect(t, z)
|
|
if data.ServicesScanned != 4 {
|
|
t.Errorf("ServicesScanned = %d, want 4", data.ServicesScanned)
|
|
}
|
|
if len(data.Findings) != 0 {
|
|
t.Errorf("Findings = %+v, want empty", data.Findings)
|
|
}
|
|
if len(data.CollectErrors) != 0 {
|
|
t.Errorf("CollectErrors = %v, want empty (modern services must be skipped silently)", data.CollectErrors)
|
|
}
|
|
}
|
|
|
|
func TestCollect_DetectsCommonLegacyTypes(t *testing.T) {
|
|
z := &rawZone{
|
|
Services: map[string][]rawService{
|
|
"": {orphanService(dns.TypeSPF, "example.com.")},
|
|
"old": {orphanService(38 /* A6 */, "old.example.com.")},
|
|
"sec": {orphanService(dns.TypeKEY, "sec.example.com."), orphanService(dns.TypeNXT, "sec.example.com.")},
|
|
"trash": {orphanService(11 /* WKS */, "trash.example.com.")},
|
|
},
|
|
}
|
|
data := runCollect(t, z)
|
|
|
|
if got := len(data.Findings); got != 5 {
|
|
t.Fatalf("Findings count = %d, want 5", got)
|
|
}
|
|
|
|
want := map[string]bool{"SPF": false, "A6": false, "KEY": false, "NXT": false, "WKS": false}
|
|
for _, f := range data.Findings {
|
|
if _, ok := want[f.TypeName]; ok {
|
|
want[f.TypeName] = true
|
|
}
|
|
}
|
|
for k, ok := range want {
|
|
if !ok {
|
|
t.Errorf("missing finding for %s", k)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEvaluate_GroupsAndRanksBySeverity(t *testing.T) {
|
|
z := &rawZone{
|
|
Services: map[string][]rawService{
|
|
"": {orphanService(dns.TypeSPF, "example.com."), orphanService(dns.TypeSPF, "example.com.")},
|
|
"a": {orphanService(dns.TypeKEY, "a.example.com.")}, // critical
|
|
"b": {orphanService(11 /* WKS */, "b.example.com.")}, // info
|
|
"c": {orphanService(11 /* WKS */, "c.example.com.")}, // info, second occurrence
|
|
"d": {orphanService(dns.TypeNULL, "d.example.com.")}, // info
|
|
},
|
|
}
|
|
data := runCollect(t, z)
|
|
|
|
// Build a fake observation getter so we can call Evaluate without spinning a host.
|
|
obs := staticObs{key: ObservationKeyLegacy, payload: mustMarshal(t, data)}
|
|
rule := &legacyRecordsRule{}
|
|
states := rule.Evaluate(context.Background(), obs, sdk.CheckerOptions{})
|
|
|
|
// 4 distinct types → 4 states.
|
|
if len(states) != 4 {
|
|
t.Fatalf("got %d states, want 4: %+v", len(states), states)
|
|
}
|
|
|
|
// First state must be the critical KEY (severity wins, not first-seen).
|
|
if states[0].Subject != "KEY" || states[0].Status != sdk.StatusCrit {
|
|
t.Errorf("top state = %+v, want KEY/Crit", states[0])
|
|
}
|
|
// SPF (warn) must come before WKS / NULL (info).
|
|
if states[1].Subject != "SPF" || states[1].Status != sdk.StatusWarn {
|
|
t.Errorf("second state = %+v, want SPF/Warn", states[1])
|
|
}
|
|
|
|
// SPF state should carry both occurrences in Meta.locations.
|
|
locs, _ := states[1].Meta["locations"].([]FindingLocation)
|
|
if len(locs) != 2 {
|
|
t.Errorf("SPF Meta.locations length = %d, want 2", len(locs))
|
|
}
|
|
}
|
|
|
|
func TestEvaluate_EmptyZoneReturnsOK(t *testing.T) {
|
|
data := &LegacyData{Zone: "example.com", ServicesScanned: 3}
|
|
obs := staticObs{key: ObservationKeyLegacy, payload: mustMarshal(t, data)}
|
|
|
|
states := (&legacyRecordsRule{}).Evaluate(context.Background(), obs, sdk.CheckerOptions{})
|
|
if len(states) != 1 || states[0].Status != sdk.StatusOK {
|
|
t.Fatalf("want single OK state, got %+v", states)
|
|
}
|
|
if !strings.Contains(states[0].Message, "3 service(s) scanned") {
|
|
t.Errorf("OK message = %q, want it to mention scanned count", states[0].Message)
|
|
}
|
|
}
|
|
|
|
func TestCollect_MissingZoneOptionFails(t *testing.T) {
|
|
p := &legacyProvider{}
|
|
_, err := p.Collect(context.Background(), sdk.CheckerOptions{})
|
|
if err == nil {
|
|
t.Fatal("expected error when 'zone' option is missing, got nil")
|
|
}
|
|
}
|
|
|
|
func TestReport_TopCardMatchesWorstSeverity(t *testing.T) {
|
|
// SPF (warn) + WKS (info) → top must be SPF.
|
|
z := &rawZone{
|
|
Services: map[string][]rawService{
|
|
"a": {orphanService(dns.TypeSPF, "a.example.com.")},
|
|
"b": {orphanService(11 /* WKS */, "b.example.com.")},
|
|
},
|
|
}
|
|
data := runCollect(t, z)
|
|
|
|
html, err := (&legacyProvider{}).GetHTMLReport(staticReportCtx{data: mustMarshal(t, data)})
|
|
if err != nil {
|
|
t.Fatalf("GetHTMLReport: %v", err)
|
|
}
|
|
if !strings.Contains(html, "Fix this first") {
|
|
t.Errorf("report missing 'Fix this first' card")
|
|
}
|
|
// The headline finding should reference SPF, not WKS.
|
|
if i, j := strings.Index(html, "Fix this first"), strings.Index(html, "Other legacy records"); i < 0 || j < 0 || !strings.Contains(html[i:j], "SPF") {
|
|
t.Errorf("'Fix this first' section does not reference SPF")
|
|
}
|
|
}
|
|
|
|
func TestReport_OKBannerWhenNoFindings(t *testing.T) {
|
|
html, err := (&legacyProvider{}).GetHTMLReport(staticReportCtx{
|
|
data: mustMarshal(t, &LegacyData{Zone: "example.com", ServicesScanned: 5}),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("GetHTMLReport: %v", err)
|
|
}
|
|
if !strings.Contains(html, "status-ok") {
|
|
t.Errorf("report missing OK banner: %q", html[:min(300, len(html))])
|
|
}
|
|
}
|
|
|
|
// --- test helpers ---------------------------------------------------------
|
|
|
|
func mustMarshal(t *testing.T, v any) []byte {
|
|
t.Helper()
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
return b
|
|
}
|
|
|
|
type staticObs struct {
|
|
key sdk.ObservationKey
|
|
payload []byte
|
|
}
|
|
|
|
func (s staticObs) Get(_ context.Context, key sdk.ObservationKey, dest any) error {
|
|
if key != s.key {
|
|
return fmt.Errorf("staticObs: unexpected observation key %q (have %q)", key, s.key)
|
|
}
|
|
return json.Unmarshal(s.payload, dest)
|
|
}
|
|
|
|
func (s staticObs) GetRelated(_ context.Context, _ sdk.ObservationKey) ([]sdk.RelatedObservation, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
type staticReportCtx struct {
|
|
data []byte
|
|
}
|
|
|
|
func (s staticReportCtx) Data() json.RawMessage { return s.data }
|
|
func (s staticReportCtx) Related(_ sdk.ObservationKey) []sdk.RelatedObservation { return nil }
|
|
func (s staticReportCtx) States() []sdk.CheckState { return nil }
|