checker-http/checker/header_rule.go

111 lines
3.4 KiB
Go

// 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,
}
})
}