checker: add domain length validation and refactor rules into per-concern checks

This commit is contained in:
nemunaire 2026-04-26 16:48:42 +07:00
commit 946ec446d2
15 changed files with 716 additions and 308 deletions

View file

@ -305,12 +305,19 @@ th { font-weight: 600; color: #6b7280; }
// GetHTMLReport implements sdk.CheckerHTMLReporter. It folds in related TLS
// observations so the XMPP service page shows cert posture directly, without
// the user having to open a separate TLS report.
//
// The hint/fix section is driven exclusively by ctx.States(): it is the host
// that has already evaluated every rule and handed us the resulting
// CheckStates. The report never re-derives issues from the raw observation
// so there is no duplicated judgment logic. When States() is empty (for
// example a standalone render with no rule run), we still show the raw
// facts (SRV table, endpoint details) but drop the actionable hints.
func (p *xmppProvider) GetHTMLReport(rctx sdk.ReportContext) (string, error) {
var d XMPPData
if err := json.Unmarshal(rctx.Data(), &d); err != nil {
return "", fmt.Errorf("unmarshal xmpp observation: %w", err)
}
view := buildReportData(&d, rctx.Related(TLSRelatedKey))
view := buildReportData(&d, rctx.Related(TLSRelatedKey), rctx.States())
return renderReport(view)
}
@ -322,12 +329,15 @@ func renderReport(view reportData) (string, error) {
return buf.String(), nil
}
func buildReportData(d *XMPPData, related []sdk.RelatedObservation) reportData {
tlsIssues := tlsIssuesFromRelated(related)
func buildReportData(d *XMPPData, related []sdk.RelatedObservation, states []sdk.CheckState) reportData {
tlsByAddr := indexTLSByAddress(related)
allIssues := append([]Issue(nil), d.Issues...)
allIssues = append(allIssues, tlsIssues...)
// Fix list comes exclusively from the CheckStates the host evaluated.
// When no states were supplied (standalone renders, one-off tests),
// the hint section is skipped entirely: we show raw facts only,
// never re-judge the observation here.
fixes := fixesFromStates(states)
hasStates := len(states) > 0
view := reportData{
Domain: d.Domain,
@ -338,35 +348,38 @@ func buildReportData(d *XMPPData, related []sdk.RelatedObservation) reportData {
HasIPv6: d.Coverage.HasIPv6,
WorkingC2S: d.Coverage.WorkingC2S,
WorkingS2S: d.Coverage.WorkingS2S,
HasIssues: len(allIssues) > 0,
HasIssues: len(fixes) > 0,
HasTLSPosture: len(tlsByAddr) > 0,
}
// Status banner.
worst := SeverityInfo
for _, is := range allIssues {
if is.Severity == SeverityCrit {
worst = SeverityCrit
break
}
if is.Severity == SeverityWarn {
worst = SeverityWarn
}
}
if len(allIssues) == 0 {
view.StatusLabel = "OK"
view.StatusClass = "ok"
// Status banner: driven by the worst CheckState when available,
// otherwise a neutral label (data-only render).
if !hasStates {
view.StatusLabel = "DATA"
view.StatusClass = "muted"
} else {
worst := sdk.StatusOK
for _, s := range states {
if s.Status > worst {
worst = s.Status
}
}
switch worst {
case SeverityCrit:
case sdk.StatusCrit, sdk.StatusError:
view.StatusLabel = "FAIL"
view.StatusClass = "fail"
case SeverityWarn:
case sdk.StatusWarn:
view.StatusLabel = "WARN"
view.StatusClass = "warn"
default:
case sdk.StatusInfo:
view.StatusLabel = "INFO"
view.StatusClass = "muted"
case sdk.StatusUnknown:
view.StatusLabel = "UNKNOWN"
view.StatusClass = "muted"
default:
view.StatusLabel = "OK"
view.StatusClass = "ok"
}
}
@ -381,16 +394,8 @@ func buildReportData(d *XMPPData, related []sdk.RelatedObservation) reportData {
return 2
}
}
sort.SliceStable(allIssues, func(i, j int) bool { return sevRank(allIssues[i].Severity) < sevRank(allIssues[j].Severity) })
for _, is := range allIssues {
view.Fixes = append(view.Fixes, reportFix{
Severity: is.Severity,
Code: is.Code,
Message: is.Message,
Fix: is.Fix,
Endpoint: is.Endpoint,
})
}
sort.SliceStable(fixes, func(i, j int) bool { return sevRank(fixes[i].Severity) < sevRank(fixes[j].Severity) })
view.Fixes = fixes
// SRV rows.
addSRV := func(prefix string, records []SRVRecord) {
@ -462,6 +467,42 @@ func buildReportData(d *XMPPData, related []sdk.RelatedObservation) reportData {
return view
}
// fixesFromStates turns CheckStates handed to us by the host into the
// severity-tagged entries rendered in the "What to fix" section. It is
// intentionally the only source of hints on the report: the raw
// observation is never re-judged here.
func fixesFromStates(states []sdk.CheckState) []reportFix {
var out []reportFix
for _, s := range states {
var sev string
switch s.Status {
case sdk.StatusCrit, sdk.StatusError:
sev = SeverityCrit
case sdk.StatusWarn:
sev = SeverityWarn
case sdk.StatusInfo:
sev = SeverityInfo
default:
// OK / Unknown: not an actionable finding.
continue
}
fix := ""
if s.Meta != nil {
if v, ok := s.Meta["fix"].(string); ok {
fix = v
}
}
out = append(out, reportFix{
Severity: sev,
Code: s.Code,
Message: s.Message,
Fix: fix,
Endpoint: s.Subject,
})
}
return out
}
func modeLabel(m XMPPMode) string {
switch m {
case ModeClient: