Use ctx.States() for remediation cards in HTML report
All checks were successful
continuous-integration/drone/push Build is passing

Gate each remediation on the presence of the corresponding rule state
code rather than re-deriving from raw observation fields; falls back to
raw-data analysis when states are absent.
This commit is contained in:
nemunaire 2026-05-18 10:59:05 +08:00
commit de87a8fef1

View file

@ -180,7 +180,7 @@ func (p *kerberosProvider) GetHTMLReport(rctx sdk.ReportContext) (string, error)
// Detect common failures and build the remediation banner. Hints are // Detect common failures and build the remediation banner. Hints are
// only surfaced when the host supplied rule states for this run. // only surfaced when the host supplied rule states for this run.
if hasStates { if hasStates {
rd.Remediations = buildRemediations(&r, rd) rd.Remediations = buildRemediations(&r, rd, states)
} }
var buf strings.Builder var buf strings.Builder
@ -190,20 +190,21 @@ func (p *kerberosProvider) GetHTMLReport(rctx sdk.ReportContext) (string, error)
return buf.String(), nil return buf.String(), nil
} }
// buildRemediations inspects the observation and returns actionable hints // buildRemediations returns actionable hints keyed by rule state codes so the
// for the user-visible failures. Only matching hints are appended, so a // report and the rule output never disagree on which conditions are present.
// healthy realm shows an empty list (rendered as nothing). // Display-only fields (realm name, clock skew string, unresolved host list)
func buildRemediations(r *KerberosData, rd reportData) []remediation { // are still read from raw data — they are presentation, not verdict.
var out []remediation func buildRemediations(r *KerberosData, rd reportData, states []sdk.CheckState) []remediation {
byCode := make(map[string]sdk.CheckState, len(states))
hasKDCSRV := false for _, st := range states {
for _, b := range r.SRV { if st.Status == sdk.StatusCrit || st.Status == sdk.StatusWarn || st.Status == sdk.StatusError {
if strings.HasPrefix(b.Prefix, "_kerberos.") && len(b.Records) > 0 { byCode[st.Code] = st
hasKDCSRV = true
break
} }
} }
if !hasKDCSRV {
var out []remediation
if _, ok := byCode[CodeNoSRV]; ok {
out = append(out, remediation{ out = append(out, remediation{
Title: "Publish Kerberos SRV records", Title: "Publish Kerberos SRV records",
Body: template.HTML(fmt.Sprintf( Body: template.HTML(fmt.Sprintf(
@ -213,41 +214,31 @@ _kerberos._udp.%[1]s. IN SRV 0 0 88 kdc.%[1]s.</pre>`, strings.ToLower(r.Realm)
}) })
} }
// SRV targets that don't resolve. if _, ok := byCode[CodeKDCUnreachable]; ok {
var unresolved []string // Use raw resolution data to show which hosts are unresolved (display only).
for _, h := range rd.Resolution { var unresolved []string
if h.Error != "" || (len(h.IPv4) == 0 && len(h.IPv6) == 0) { for _, h := range rd.Resolution {
unresolved = append(unresolved, h.Target) if h.Error != "" || (len(h.IPv4) == 0 && len(h.IPv6) == 0) {
} unresolved = append(unresolved, h.Target)
}
if len(unresolved) > 0 {
out = append(out, remediation{
Title: "Resolve KDC host names",
Body: template.HTML(fmt.Sprintf(
`The following SRV target(s) do not resolve to an IP address: <code>%s</code>. Add A/AAAA records for each host, or correct the SRV target.`,
template.HTMLEscapeString(strings.Join(unresolved, ", ")))),
})
}
// No KDC reachable (port filtered / host down).
reachable := 0
totalKDC := 0
for _, p := range r.Probes {
if p.Role == "kdc" {
totalKDC++
if p.OK {
reachable++
} }
} }
} if len(unresolved) > 0 {
if totalKDC > 0 && reachable == 0 { out = append(out, remediation{
out = append(out, remediation{ Title: "Resolve KDC host names",
Title: "Open port 88 on KDC hosts", Body: template.HTML(fmt.Sprintf(
Body: template.HTML(`Every KDC endpoint refused or timed out. Ensure your firewall allows inbound <code>TCP 88</code> and <code>UDP 88</code>, and that the KDC process is listening on the SRV target's IP.`), `The following SRV target(s) do not resolve to an IP address: <code>%s</code>. Add A/AAAA records for each host, or correct the SRV target.`,
}) template.HTMLEscapeString(strings.Join(unresolved, ", ")))),
})
} else {
out = append(out, remediation{
Title: "Open port 88 on KDC hosts",
Body: template.HTML(`Every KDC endpoint refused or timed out. Ensure your firewall allows inbound <code>TCP 88</code> and <code>UDP 88</code>, and that the KDC process is listening on the SRV target's IP.`),
})
}
} }
// Clock skew. // Clock skew is already driven by CodeClockSkewBad in the caller; pass
// the display-enriched flag through rather than re-checking states here.
if rd.ClockSkewBad { if rd.ClockSkewBad {
out = append(out, remediation{ out = append(out, remediation{
Title: "Synchronize clocks", Title: "Synchronize clocks",
@ -257,8 +248,7 @@ _kerberos._udp.%[1]s. IN SRV 0 0 88 kdc.%[1]s.</pre>`, strings.ToLower(r.Realm)
}) })
} }
// Weak crypto only. if _, ok := byCode[CodeEnctypesWeakOnly]; ok {
if rd.HasWeakOnly {
out = append(out, remediation{ out = append(out, remediation{
Title: "Retire DES/RC4 enctypes", Title: "Retire DES/RC4 enctypes",
Body: template.HTML(`The KDC only advertises weak encryption types. Configure at least AES: Body: template.HTML(`The KDC only advertises weak encryption types. Configure at least AES:
@ -270,8 +260,7 @@ then rekey principals with <code>kadmin -q "cpw -randkey principal"</code> or eq
}) })
} }
// Wrong realm in KDC reply. if _, ok := byCode[CodeASWrongRealm]; ok {
if r.AS.ServerRealm != "" && !strings.EqualFold(r.AS.ServerRealm, r.Realm) {
out = append(out, remediation{ out = append(out, remediation{
Title: "Fix realm mismatch", Title: "Fix realm mismatch",
Body: template.HTML(fmt.Sprintf( Body: template.HTML(fmt.Sprintf(
@ -281,8 +270,7 @@ then rekey principals with <code>kadmin -q "cpw -randkey principal"</code> or eq
}) })
} }
// AS-REP without preauth, AS-REP roasting. if _, ok := byCode[CodeASRepNoPreauth]; ok {
if r.AS.Attempted && r.AS.PrincipalFound && !r.AS.PreauthReq {
out = append(out, remediation{ out = append(out, remediation{
Title: "Enable pre-authentication", Title: "Enable pre-authentication",
Body: template.HTML(`The KDC returned an AS-REP without demanding pre-authentication. This exposes principals to <strong>AS-REP roasting</strong>. Enable <code>requires_preauth</code> (MIT) or the equivalent flag on every user principal, e.g. <code>kadmin -q "modprinc +requires_preauth user@REALM"</code>.`), Body: template.HTML(`The KDC returned an AS-REP without demanding pre-authentication. This exposes principals to <strong>AS-REP roasting</strong>. Enable <code>requires_preauth</code> (MIT) or the equivalent flag on every user principal, e.g. <code>kadmin -q "modprinc +requires_preauth user@REALM"</code>.`),