Deepen CSP, Permissions-Policy and cookie audits
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.
This commit is contained in:
parent
27a30638f4
commit
603e93355b
8 changed files with 738 additions and 305 deletions
|
|
@ -11,50 +11,71 @@ import (
|
|||
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.
|
||||
// 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.
|
||||
//
|
||||
// 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
|
||||
// Three callbacks cover the spectrum, from simplest to most expressive:
|
||||
//
|
||||
// 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.
|
||||
// - 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 is the rule's Name() and the prefix for every CheckState
|
||||
// code it emits.
|
||||
Code string
|
||||
|
||||
// Description is returned by Description().
|
||||
Code string
|
||||
Description string
|
||||
Header 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 toggles the severity of the default ".missing" emitter
|
||||
// (Warn when true, Info when false). Ignored when OnMissing is set.
|
||||
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, 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}
|
||||
}
|
||||
|
||||
|
|
@ -63,49 +84,90 @@ 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 {
|
||||
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)
|
||||
|
||||
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
|
||||
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))
|
||||
}
|
||||
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,
|
||||
}
|
||||
})
|
||||
}
|
||||
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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue