diff --git a/README.md b/README.md index 98bd6aa..137012c 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ relies on TLS for transport. | `http.x_frame_options` | Verifies that responses set X-Frame-Options or a CSP frame-ancestors directive. | WARNING | | `http.x_content_type_options` | Verifies that responses set X-Content-Type-Options: nosniff. | WARNING | | `http.x_xss_protection` | Reports the legacy X-XSS-Protection header; warns on filtering mode (can introduce XSS), absent/`0` are fine, CSP is the real defense. | WARNING | +| `http.expect_ct` | Flags the deprecated Expect-CT header (Certificate Transparency is now enforced by mainstream clients; Mozilla recommends removing it). | WARNING | | `http.referrer_policy` | Verifies that responses set a Referrer-Policy header with a privacy-preserving value. | WARNING | | `http.permissions_policy` | Verifies that the Permissions-Policy header restricts powerful APIs (camera, microphone, geolocation, …). | WARNING | | `http.coop` | Verifies the Cross-Origin-Opener-Policy (COOP) header for cross-origin process isolation. | WARNING | diff --git a/checker/rules_security_headers.go b/checker/rules_security_headers.go index d5bed10..7b7eaa8 100644 --- a/checker/rules_security_headers.go +++ b/checker/rules_security_headers.go @@ -69,6 +69,27 @@ func init() { }} }, })) + + RegisterRule(HeaderRule(HeaderRuleSpec{ + Code: "http.expect_ct", + Description: "Reports the presence of the deprecated Expect-CT header (Certificate Transparency enforcement is now mandatory in mainstream clients; Mozilla recommends removing it).", + Header: "Expect-CT", + Inspect: func(_ string, _ HTTPProbe, _ sdk.CheckerOptions) []HeaderResult { + return []HeaderResult{{ + Status: sdk.StatusWarn, + Suffix: "deprecated", + Message: "Expect-CT is deprecated. Certificate Transparency is now enforced by mainstream clients, so the header serves no purpose; Mozilla recommends removing it.", + Meta: map[string]any{"fix": "Remove the `Expect-CT` header from your responses."}, + }} + }, + OnMissing: func(_ HTTPProbe, _ sdk.CheckerOptions) []HeaderResult { + return []HeaderResult{{ + Status: sdk.StatusOK, + Suffix: "absent", + Message: "Expect-CT is not set, which is correct (the header is deprecated).", + }} + }, + })) } // HSTS ---------------------------------------------------------------- diff --git a/checker/rules_security_headers_test.go b/checker/rules_security_headers_test.go index a6587a4..e0e305d 100644 --- a/checker/rules_security_headers_test.go +++ b/checker/rules_security_headers_test.go @@ -279,6 +279,23 @@ func TestXXSSProtectionRule(t *testing.T) { } } +func TestExpectCTRule(t *testing.T) { + // Absent → OK, since Expect-CT is deprecated. + states := runRule(t, ruleByName(t, "http.expect_ct"), &HTTPData{Probes: []HTTPProbe{httpsProbe("a:443")}}, nil) + mustStatus(t, states, sdk.StatusOK) + if !hasCode(states, "http.expect_ct.absent") { + t.Errorf("missing absent code: %+v", states) + } + // Present → Warn deprecated. + p := httpsProbe("a:443") + p.Headers["expect-ct"] = "max-age=86400, enforce" + states = runRule(t, ruleByName(t, "http.expect_ct"), &HTTPData{Probes: []HTTPProbe{p}}, nil) + mustStatus(t, states, sdk.StatusWarn) + if !hasCode(states, "http.expect_ct.deprecated") { + t.Errorf("missing deprecated code: %+v", states) + } +} + func TestSecurityHeaders_NoHTTPS(t *testing.T) { // Each header rule must emit Unknown when there are no successful HTTPS probes. rules := []sdk.CheckRule{ @@ -287,6 +304,7 @@ func TestSecurityHeaders_NoHTTPS(t *testing.T) { ruleByName(t, "http.x_frame_options"), ruleByName(t, "http.x_content_type_options"), ruleByName(t, "http.x_xss_protection"), + ruleByName(t, "http.expect_ct"), } data := &HTTPData{Probes: []HTTPProbe{httpProbe("a:80")}} for _, r := range rules {