package checker import ( "bytes" "encoding/json" "fmt" "html/template" sdk "git.happydns.org/checker-sdk-go/checker" ) // GetHTMLReport renders the dangling-records observation as HTML. 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 per-owner cards from CheckState.Meta so the report and rule never disagree. 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 = `
{{.Zone}} · {{end}}{{.ServicesScanned}} service(s) scanned · {{.Pointers}} pointer(s) inspected{{.Top.Owner}}
{{.Top.Severity}}
| Pointer | Target | Why |
|---|---|---|
{{.Rrtype}} |
{{.Target}} |
{{.Reason}}{{if .Detail}} ({{.Detail}}){{end}} |
{{.Owner}}
{{.Severity}}
{{.Rrtype}} → {{.Target}}: {{.Reason}}