Use ctx.States() for remediation cards in HTML report
All checks were successful
continuous-integration/drone/push Build is passing
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:
parent
b3a13168de
commit
de87a8fef1
1 changed files with 38 additions and 50 deletions
|
|
@ -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>.`),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue