// 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" ) // HeaderRuleSpec declares a "presence + value validation" rule for one // HTTP response header. It covers the most common shape of security // header rule (one of Referrer-Policy, Permissions-Policy, COOP, COEP, // CORP, X-Content-Type-Options, …) without forcing the author to write // the load/iterate/build-state scaffolding. // // The DSL emits three CheckState codes derived from Code: // - Code+".missing" when the header is absent // - Code+".invalid" when Validate returns a non-OK status // - Code+".ok" when Validate accepts the value // // Rules with richer semantics (HSTS quality thresholds, CSP directive // inspection, cookie flag aggregation, legacy headers with reversed // "absent is fine" semantics) keep implementing sdk.CheckRule directly. type HeaderRuleSpec struct { // Code is the rule's Name() and the prefix for every CheckState // code it emits. Code string // Description is returned by Description(). Description string // Header is the response header to inspect. Lookups go through the // lowercased map populated by the collector, so casing is flexible. Header string // Required toggles the severity of an absent header: Warn when true, // Info when false. Required bool // Validate, when set, inspects the trimmed header value. Return // (StatusOK, msg) to accept the value (emits ".ok" with msg) or any // other status to flag it (emits ".invalid" with msg). When nil, // presence alone is treated as OK with a generic message. Validate func(value string) (sdk.Status, string) // FixHint, when set, is attached as Meta.fix on the ".missing" // state. FixHint string } // HeaderRule constructs a self-contained sdk.CheckRule from a spec. // Intended to be wired in init() via RegisterRule. func HeaderRule(spec HeaderRuleSpec) sdk.CheckRule { 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, _ sdk.CheckerOptions) []sdk.CheckState { data, errSt := loadHTTPData(ctx, obs) if errSt != nil { return []sdk.CheckState{*errSt} } headerKey := strings.ToLower(r.spec.Header) return EvalPerHTTPS(data, r.spec.Code, func(p HTTPProbe) sdk.CheckState { v := strings.TrimSpace(p.Headers[headerKey]) if v == "" { status := sdk.StatusWarn if !r.spec.Required { status = sdk.StatusInfo } st := sdk.CheckState{ Status: status, Code: r.spec.Code + ".missing", Subject: p.Address, Message: r.spec.Header + " is not set.", } if r.spec.FixHint != "" { st.Meta = map[string]any{"fix": r.spec.FixHint} } return st } if r.spec.Validate == nil { return sdk.CheckState{ Status: sdk.StatusOK, Code: r.spec.Code + ".ok", Subject: p.Address, Message: r.spec.Header + " is set.", } } status, msg := r.spec.Validate(v) suffix := ".invalid" if status == sdk.StatusOK { suffix = ".ok" } return sdk.CheckState{ Status: status, Code: r.spec.Code + suffix, Subject: p.Address, Message: msg, } }) }