111 lines
3.4 KiB
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,
|
|
}
|
|
})
|
|
}
|