// This file is part of the happyDomain (R) project. // Copyright (c) 2020-2026 happyDomain // Authors: Pierre-Olivier Mercier, et al. package checker import ( "context" "strings" sdk "git.happydns.org/checker-sdk-go/checker" ) // HeaderResult is one observation produced by a HeaderRuleSpec callback. // Suffix is appended to spec.Code (with a dot separator) to form the // final CheckState code, e.g. "http.hsts" + "short_max_age" → // "http.hsts.short_max_age". An empty Suffix uses spec.Code verbatim. type HeaderResult struct { Status sdk.Status Suffix string Message string Meta map[string]any } // HeaderRuleSpec declares a per-HTTPS-probe rule built around a single // response header. It supersedes the per-rule Evaluate boilerplate that // every "load HTTPData → iterate successful HTTPS probes → inspect one // header → emit one CheckState" rule used to repeat. // // Three callbacks cover the spectrum, from simplest to most expressive: // // - Validate: the header is present and a single boolean verdict is // enough. Returns (Status, message); the rule emits ".ok" on // StatusOK or ".invalid" otherwise. Used by the modern privacy // headers (Referrer-Policy, COOP/COEP/CORP, Permissions-Policy). // // - Inspect: the header is present and may produce any number of // findings with arbitrary suffixes. Used by HSTS (".short_max_age"), // CSP (".unsafe_inline" / ".wildcard_script_src" / …) and the // legacy X-XSS-Protection rule which reports custom suffixes // (".disabled", ".enabled"). // // - OnMissing: the header is absent and the default ".missing" // emitter is wrong — either an alternative satisfies the // requirement (CSP frame-ancestors standing in for X-Frame-Options), // or absence has non-default severity (X-XSS-Protection emits // Info ".absent", not Warn ".missing"), or the severity depends // on a CheckerOption (HSTS/CSP gate "missing" on a configurable // "required" flag). // // Validate and Inspect are mutually exclusive. OnMissing can be combined // with either. Specs that omit all three behave as a pure presence check // (".ok" when set, default ".missing" when not). type HeaderRuleSpec struct { Code string Description string Header string // Required toggles the severity of the default ".missing" emitter // (Warn when true, Info when false). Ignored when OnMissing is set. Required bool // FixHint, when set, populates Meta.fix on the default ".missing" // emitter. Ignored when OnMissing is set (callbacks must build // their own Meta). FixHint string Validate func(value string) (sdk.Status, string) Inspect func(value string, p HTTPProbe, opts sdk.CheckerOptions) []HeaderResult OnMissing func(p HTTPProbe, opts sdk.CheckerOptions) []HeaderResult } // HeaderRule constructs a self-contained sdk.CheckRule from a spec. // Intended to be wired in init() via RegisterRule. func HeaderRule(spec HeaderRuleSpec) sdk.CheckRule { if spec.Validate != nil && spec.Inspect != nil { panic("checker: HeaderRuleSpec " + spec.Code + " sets both Validate and Inspect") } return &headerRule{spec: spec} } type headerRule struct{ spec HeaderRuleSpec } func (r *headerRule) Name() string { return r.spec.Code } func (r *headerRule) Description() string { return r.spec.Description } func (r *headerRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState { data, errSt := loadHTTPData(ctx, obs) if errSt != nil { return []sdk.CheckState{*errSt} } probes := successfulHTTPSProbes(data.Probes) if len(probes) == 0 { return []sdk.CheckState{unknownState(r.spec.Code+".no_https", "No successful HTTPS probe to evaluate.")} } headerKey := strings.ToLower(r.spec.Header) out := make([]sdk.CheckState, 0, len(probes)) for _, p := range probes { for _, res := range r.evaluateProbe(p, opts, headerKey) { out = append(out, r.toCheckState(p, res)) } } return out } func (r *headerRule) evaluateProbe(p HTTPProbe, opts sdk.CheckerOptions, headerKey string) []HeaderResult { v := strings.TrimSpace(p.Headers[headerKey]) if v == "" { if r.spec.OnMissing != nil { return ensureNonEmpty(r.spec.OnMissing(p, opts), r.defaultPresent()) } return []HeaderResult{r.defaultMissing()} } switch { case r.spec.Inspect != nil: return ensureNonEmpty(r.spec.Inspect(v, p, opts), r.defaultPresent()) case r.spec.Validate != nil: status, msg := r.spec.Validate(v) suffix := "invalid" if status == sdk.StatusOK { suffix = "ok" } return []HeaderResult{{Status: status, Suffix: suffix, Message: msg}} default: return []HeaderResult{r.defaultPresent()} } } func (r *headerRule) defaultMissing() HeaderResult { status := sdk.StatusInfo if r.spec.Required { status = sdk.StatusWarn } res := HeaderResult{ Status: status, Suffix: "missing", Message: r.spec.Header + " is not set.", } if r.spec.FixHint != "" { res.Meta = map[string]any{"fix": r.spec.FixHint} } return res } func (r *headerRule) defaultPresent() HeaderResult { return HeaderResult{ Status: sdk.StatusOK, Suffix: "ok", Message: r.spec.Header + " is set.", } } func (r *headerRule) toCheckState(p HTTPProbe, res HeaderResult) sdk.CheckState { code := r.spec.Code if res.Suffix != "" { code = code + "." + res.Suffix } return sdk.CheckState{ Status: res.Status, Code: code, Subject: p.Address, Message: res.Message, Meta: res.Meta, } } func ensureNonEmpty(results []HeaderResult, fallback HeaderResult) []HeaderResult { if len(results) == 0 { return []HeaderResult{fallback} } return results }