From 16497f2ec1dc47d623bf90fc88f04dcc77954431 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Mon, 18 May 2026 15:30:19 +0800 Subject: [PATCH] Drive GetHTMLReport from ctx.States() --- checker/report.go | 172 ++++++++++++++++++++++++++++-------------- checker/rules_test.go | 10 ++- 2 files changed, 123 insertions(+), 59 deletions(-) diff --git a/checker/report.go b/checker/report.go index 12f4352..469938f 100644 --- a/checker/report.go +++ b/checker/report.go @@ -12,11 +12,10 @@ import ( // GetHTMLReport renders the legacy-records observation as a self-contained // HTML page suitable for iframe embedding. // -// The "fix this first" card is driven by the most-severe finding (no fixed -// rule wins by name): SeverityCrit > SeverityWarn > SeverityInfo, with the -// alphabetically-first type name as a stable tie-break. This matches what -// the rule sorter produces, so the top card and the rule output never -// disagree on which finding is "the" priority. +// Cards are built from ctx.States(): each finding state carries the full +// metadata (reason, replacement, how-to-fix, locations) in CheckState.Meta. +// The rule already emits states in descending severity order, so the first +// card is always the top priority without re-sorting here. func (p *legacyProvider) GetHTMLReport(ctx sdk.ReportContext) (string, error) { var data LegacyData if raw := ctx.Data(); len(raw) > 0 { @@ -65,48 +64,131 @@ func buildReportView(data *LegacyData, states []sdk.CheckState) *reportView { CollectErrors: data.CollectErrors, } - groups := groupFindings(data.Findings) - cards := make([]findingCard, 0, len(groups)) - worst := SeverityInfo - for _, g := range groups { - info := deprecatedTypes[g.Rrtype] - if info.Severity > worst { - worst = info.Severity + if len(states) == 0 { + // No rule output yet: data-only rendering with a neutral headline. + v.OverallStatus = "ok" + v.OverallText = fmt.Sprintf("Data collected — %d service(s) scanned.", data.ServicesScanned) + v.OverallClass = "status-ok" + return v + } + + var cards []findingCard + for _, st := range states { + c, ok := cardFromState(st) + if !ok { + continue } - cards = append(cards, findingCard{ - TypeName: g.TypeName, - Reason: info.Reason, - Replacement: info.Replacement, - HowToFix: info.HowToFix, - Severity: severityLabel(info.Severity), - SeverityCSS: info.Severity.String(), - Count: len(g.Locations), - Locations: g.Locations, - }) + cards = append(cards, c) } if len(cards) > 0 { v.Top = &cards[0] v.Others = cards[1:] - v.OverallStatus = worst.String() - v.OverallText, v.OverallClass = overallLabel(worst) + worst := worstFindingStatus(states) + v.OverallStatus, v.OverallText, v.OverallClass = overallFromStatus(worst) + } else if errState, ok := firstErrorState(states); ok { + v.OverallStatus = "error" + v.OverallText = errState.Message + v.OverallClass = "status-crit" } else { - // Honour the rule's status when present: an Error from the rule - // (e.g. observation load failure) must not be masked as "OK". - if errState, ok := firstErrorState(states); ok { - v.OverallStatus = "error" - v.OverallText = errState.Message - v.OverallClass = "status-crit" - } else { - v.OverallStatus = "ok" - v.OverallText = fmt.Sprintf("No legacy record types found across %d service(s).", data.ServicesScanned) - v.OverallClass = "status-ok" + v.OverallStatus = "ok" + v.OverallClass = "status-ok" + v.OverallText = fmt.Sprintf("No legacy record types found across %d service(s).", data.ServicesScanned) + for _, st := range states { + if st.Code == "legacy_records_clean" { + v.OverallText = st.Message + break + } } } return v } +// cardFromState builds a findingCard from a finding CheckState. States that +// carry no "locations" metadata (clean / error / skip states) return ok=false. +func cardFromState(st sdk.CheckState) (findingCard, bool) { + if st.Meta == nil { + return findingCard{}, false + } + rawLocs, ok := st.Meta["locations"] + if !ok { + return findingCard{}, false + } + locations := decodeLocations(rawLocs) + + typeName, _ := st.Meta["type"].(string) + if typeName == "" { + typeName = st.Subject + } + reason, _ := st.Meta["reason"].(string) + replacement, _ := st.Meta["replacement"].(string) + howToFix, _ := st.Meta["how_to_fix"].(string) + sevLabel, sevCSS := severityFromStatus(st.Status) + + return findingCard{ + TypeName: typeName, + Reason: reason, + Replacement: replacement, + HowToFix: howToFix, + Severity: sevLabel, + SeverityCSS: sevCSS, + Count: len(locations), + Locations: locations, + }, true +} + +// decodeLocations handles the JSON round-trip: Meta values are stored as any +// but pass through json.Marshal/Unmarshal when transmitted, so []FindingLocation +// arrives as []interface{} of map[string]interface{}. Re-marshaling restores +// the typed slice. +func decodeLocations(v any) []FindingLocation { + b, err := json.Marshal(v) + if err != nil { + return nil + } + var locs []FindingLocation + if err := json.Unmarshal(b, &locs); err != nil { + return nil + } + return locs +} + +func severityFromStatus(s sdk.Status) (label, css string) { + switch s { + case sdk.StatusCrit, sdk.StatusError: + return "Critical", "crit" + case sdk.StatusWarn: + return "Warning", "warn" + default: + return "Informational", "info" + } +} + +func worstFindingStatus(states []sdk.CheckState) sdk.Status { + worst := sdk.StatusInfo + for _, st := range states { + switch st.Status { + case sdk.StatusCrit, sdk.StatusError: + return sdk.StatusCrit + case sdk.StatusWarn: + worst = sdk.StatusWarn + } + } + return worst +} + +func overallFromStatus(s sdk.Status) (status, text, css string) { + switch s { + case sdk.StatusCrit, sdk.StatusError: + return "crit", "Legacy records require urgent migration", "status-crit" + case sdk.StatusWarn: + return "warn", "Legacy records should be migrated", "status-warn" + default: + return "info", "Only informational legacy records found", "status-info" + } +} + func firstErrorState(states []sdk.CheckState) (sdk.CheckState, bool) { for i := range states { if states[i].Status == sdk.StatusError { @@ -116,28 +198,6 @@ func firstErrorState(states []sdk.CheckState) (sdk.CheckState, bool) { return sdk.CheckState{}, false } -func severityLabel(s DeprecatedSeverity) string { - switch s { - case SeverityCrit: - return "Critical" - case SeverityWarn: - return "Warning" - default: - return "Informational" - } -} - -func overallLabel(s DeprecatedSeverity) (text, css string) { - switch s { - case SeverityCrit: - return "Legacy records require urgent migration", "status-crit" - case SeverityWarn: - return "Legacy records should be migrated", "status-warn" - default: - return "Only informational legacy records found", "status-info" - } -} - var reportTmpl = template.Must(template.New("legacy-records-report").Funcs(template.FuncMap{ "display": func(s string) string { if s == "" || s == "@" { diff --git a/checker/rules_test.go b/checker/rules_test.go index c176752..15b1e76 100644 --- a/checker/rules_test.go +++ b/checker/rules_test.go @@ -184,8 +184,11 @@ func TestReport_TopCardMatchesWorstSeverity(t *testing.T) { }, } data := runCollect(t, z) + raw := mustMarshal(t, data) + states := (&legacyRecordsRule{}).Evaluate(context.Background(), + staticObs{key: ObservationKeyLegacy, payload: raw}, sdk.CheckerOptions{}) - html, err := (&legacyProvider{}).GetHTMLReport(staticReportCtx{data: mustMarshal(t, data)}) + html, err := (&legacyProvider{}).GetHTMLReport(staticReportCtx{data: raw, states: states}) if err != nil { t.Fatalf("GetHTMLReport: %v", err) } @@ -238,9 +241,10 @@ func (s staticObs) GetRelated(_ context.Context, _ sdk.ObservationKey) ([]sdk.Re } type staticReportCtx struct { - data []byte + data []byte + states []sdk.CheckState } func (s staticReportCtx) Data() json.RawMessage { return s.data } func (s staticReportCtx) Related(_ sdk.ObservationKey) []sdk.RelatedObservation { return nil } -func (s staticReportCtx) States() []sdk.CheckState { return nil } +func (s staticReportCtx) States() []sdk.CheckState { return s.states }