package checker import ( "bytes" "encoding/json" "fmt" "html/template" sdk "git.happydns.org/checker-sdk-go/checker" ) // GetHTMLReport renders the dangling-records observation as a // self-contained HTML page. The report shows one card per impacted // owner, sorted by descending severity, with the failing pointer and // the human-readable reason behind each trigger. func (p *danglingProvider) GetHTMLReport(ctx sdk.ReportContext) (string, error) { var data DanglingData if raw := ctx.Data(); len(raw) > 0 { if err := json.Unmarshal(raw, &data); err != nil { return "", fmt.Errorf("parse dangling-records data: %w", err) } } view := buildReportView(&data, ctx.States()) buf := &bytes.Buffer{} if err := reportTmpl.Execute(buf, view); err != nil { return "", err } return buf.String(), nil } type reportView struct { Zone string ServicesScanned int Pointers int OverallText string OverallClass string Top *ownerCard Others []ownerCard CollectErrors []string } type ownerCard struct { Owner string Severity string SeverityCSS string Triggers []SignalTrigger } func buildReportView(data *DanglingData, states []sdk.CheckState) *reportView { v := &reportView{ Zone: data.Zone, ServicesScanned: data.ServicesScanned, Pointers: len(data.Pointers), CollectErrors: data.CollectErrors, } cards := cardsFromStates(states) if len(cards) == 0 { // Honour an Error state from the rule so the banner does not // masquerade as OK when the observation could not be loaded. if errState, ok := firstErrorState(states); ok { v.OverallText = errState.Message v.OverallClass = "status-crit" return v } v.OverallText = fmt.Sprintf("No dangling subdomain detected across %d service(s).", data.ServicesScanned) v.OverallClass = "status-ok" return v } v.Top = &cards[0] v.Others = cards[1:] v.OverallText, v.OverallClass = overallLabel(cards[0].SeverityCSS) return v } // cardsFromStates rebuilds the per-owner cards from the CheckState // slice the host has already produced. We rely on Meta.triggers (set by // danglingRule.Evaluate) so the report and the rule never disagree on // what to show. func cardsFromStates(states []sdk.CheckState) []ownerCard { out := make([]ownerCard, 0, len(states)) for _, st := range states { if st.Code == "dangling_clean" || st.Code == "dangling_observation_error" { continue } card := ownerCard{ Owner: st.Subject, } if sev, ok := st.Meta["severity"].(string); ok { card.Severity = severityLabel(sev) card.SeverityCSS = sev } // Triggers may have been round-tripped through JSON if the host // crossed an HTTP boundary; handle both shapes. switch v := st.Meta["triggers"].(type) { case []SignalTrigger: card.Triggers = v case []any: skipped := 0 for _, item := range v { b, err := json.Marshal(item) if err != nil { skipped++ continue } var t SignalTrigger if err := json.Unmarshal(b, &t); err != nil { skipped++ continue } card.Triggers = append(card.Triggers, t) } if skipped > 0 { card.Triggers = append(card.Triggers, SignalTrigger{ Reason: fmt.Sprintf("%d trigger(s) could not be rendered.", skipped), }) } } out = append(out, card) } return out } func firstErrorState(states []sdk.CheckState) (sdk.CheckState, bool) { for i := range states { if states[i].Status == sdk.StatusError { return states[i], true } } return sdk.CheckState{}, false } func severityLabel(css string) string { switch css { case "critical": return "Critical" case "warning": return "Warning" case "info": return "Informational" default: return "" } } func overallLabel(severityCSS string) (text, css string) { switch severityCSS { case "critical": return "Dangling subdomains require urgent attention", "status-crit" case "warning": return "Dangling subdomains should be reviewed", "status-warn" case "info": return "Informational pointer issues found", "status-info" default: return "Dangling subdomains detected", "status-warn" } } var reportTmpl = template.Must(template.New("dangling-records-report").Parse(reportTemplate)) const reportTemplate = ` Dangling subdomains — {{if .Zone}}{{.Zone}}{{else}}zone report{{end}}

Dangling subdomains

{{if .Zone}}Zone: {{.Zone}} · {{end}}{{.ServicesScanned}} service(s) scanned · {{.Pointers}} pointer(s) inspected
{{.OverallText}}
{{if .Top}}

Fix this first

{{.Top.Owner}} {{.Top.Severity}}

{{if .Top.Triggers}} {{range .Top.Triggers}} {{end}}
PointerTargetWhy
{{.Rrtype}} {{.Target}} {{.Reason}}{{if .Detail}} ({{.Detail}}){{end}}
{{end}}
{{end}} {{if .Others}}

Other dangling subdomains

{{range .Others}}

{{.Owner}} {{.Severity}}

{{if .Triggers}} {{end}}
{{end}} {{end}} {{if .CollectErrors}}
{{len .CollectErrors}} service(s) skipped during scan
{{end}} `