Detect CSP weaknesses individually (unsafe-inline, unsafe-eval, missing default-src/script-src, permissive sources on script-src or its default-src fallback) instead of a single catch-all "unsafe" code, and honour CSP3 fetch-directive fallback via EffectiveSources/WildcardSource helpers. Validate Permissions-Policy values: warn when a powerful feature (camera, microphone, geolocation, payment, sensors, …) is granted to all origins. Add a SameSite aggregate state on cookie audits so callers get the global ratio alongside per-cookie diagnostics.
173 lines
5.5 KiB
Go
173 lines
5.5 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"
|
|
)
|
|
|
|
// 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
|
|
}
|