///|
/// Header and CSP policy builders.
///
/// This module lets callers generate deterministic response-header baselines
/// instead of copying long strings between services and CI fixtures.
pub(all) enum HeaderPlanKind {
HeaderPlanStaticSite
HeaderPlanSpaApp
HeaderPlanApiService
HeaderPlanAdminConsole
HeaderPlanLoginFlow
HeaderPlanMediaCdn
HeaderPlanCustom
} derive(Eq, Debug)
///|
pub(all) enum ValidationLevel {
ValidationError
ValidationWarning
ValidationNote
} derive(Eq, Debug)
///|
pub(all) struct HeaderPair {
name : String
value : String
} derive(Eq, Debug)
///|
pub(all) struct CspDirectiveSpec {
name : String
values : Array[String]
} derive(Eq, Debug)
///|
pub(all) struct CspBuilder {
report_only : Bool
directives : Array[CspDirectiveSpec]
} derive(Eq, Debug)
///|
pub(all) struct HstsConfig {
max_age : Int
include_subdomains : Bool
preload : Bool
} derive(Eq, Debug)
///|
pub(all) struct ValidationMessage {
level : ValidationLevel
field : String
message : String
} derive(Eq, Debug)
///|
pub(all) struct PolicyValidation {
ok : Bool
messages : Array[ValidationMessage]
} derive(Eq, Debug)
///|
pub(all) struct HeaderPlan {
kind : HeaderPlanKind
title : String
scenario : String
headers : Array[HeaderPair]
notes : Array[String]
} derive(Eq, Debug)
///|
pub fn HeaderPlanKind::label(self : HeaderPlanKind) -> String {
match self {
HeaderPlanStaticSite => "static-site"
HeaderPlanSpaApp => "spa-app"
HeaderPlanApiService => "api-service"
HeaderPlanAdminConsole => "admin-console"
HeaderPlanLoginFlow => "login-flow"
HeaderPlanMediaCdn => "media-cdn"
HeaderPlanCustom => "custom"
}
}
///|
pub fn ValidationLevel::label(self : ValidationLevel) -> String {
match self {
ValidationError => "error"
ValidationWarning => "warning"
ValidationNote => "note"
}
}
///|
pub fn header_pair(name : StringView, value : StringView) -> HeaderPair {
{ name: name.trim().to_owned(), value: value.trim().to_owned() }
}
///|
pub fn HeaderPair::line(self : HeaderPair) -> String {
self.name + ": " + self.value
}
///|
pub fn HeaderPair::normalized_name(self : HeaderPair) -> String {
normalize_header_name(self.name)
}
///|
pub fn HeaderPair::to_markdown_row(self : HeaderPair) -> String {
"| `" +
self.name +
"` | `" +
self.value.replace_all(old="|", new="\\|") +
"` |"
}
///|
pub fn new_csp_builder() -> CspBuilder {
{ report_only: false, directives: [] }
}
///|
pub fn csp_report_only_builder() -> CspBuilder {
{ report_only: true, directives: [] }
}
///|
pub fn CspBuilder::as_enforcing(self : CspBuilder) -> CspBuilder {
{ report_only: false, directives: self.copy_directives() }
}
///|
pub fn CspBuilder::as_report_only(self : CspBuilder) -> CspBuilder {
{ report_only: true, directives: self.copy_directives() }
}
///|
pub fn CspBuilder::copy_directives(
self : CspBuilder,
) -> Array[CspDirectiveSpec] {
self.directives.map(spec => { name: spec.name, values: spec.values.copy() })
}
///|
pub fn CspBuilder::with_directive(
self : CspBuilder,
name : StringView,
values : Array[String],
) -> CspBuilder {
let normalized = builder_normalize_name(name)
let next : Array[CspDirectiveSpec] = []
let mut found = false
for spec in self.directives {
if spec.name == normalized {
next.push({ name: normalized, values: values.copy() })
found = true
} else {
next.push({ name: spec.name, values: spec.values.copy() })
}
}
if !found {
next.push({ name: normalized, values: values.copy() })
}
{ report_only: self.report_only, directives: next }
}
///|
pub fn CspBuilder::append_value(
self : CspBuilder,
name : StringView,
value : StringView,
) -> CspBuilder {
let normalized = builder_normalize_name(name)
let next : Array[CspDirectiveSpec] = []
let mut found = false
for spec in self.directives {
if spec.name == normalized {
let values = spec.values.copy()
let token = value.trim().to_owned()
if !values.any(item => item == token) {
values.push(token)
}
next.push({ name: spec.name, values })
found = true
} else {
next.push({ name: spec.name, values: spec.values.copy() })
}
}
if !found {
next.push({ name: normalized, values: [value.trim().to_owned()] })
}
{ report_only: self.report_only, directives: next }
}
///|
pub fn CspBuilder::without_directive(
self : CspBuilder,
name : StringView,
) -> CspBuilder {
let normalized = builder_normalize_name(name)
{
report_only: self.report_only,
directives: self.directives
.filter(spec => spec.name != normalized)
.map(spec => { name: spec.name, values: spec.values.copy() }),
}
}
///|
pub fn CspBuilder::has_directive(self : CspBuilder, name : StringView) -> Bool {
let normalized = builder_normalize_name(name)
self.directives.any(spec => spec.name == normalized)
}
///|
pub fn CspBuilder::values_for(
self : CspBuilder,
name : StringView,
) -> Array[String] {
let normalized = builder_normalize_name(name)
match self.directives.iter().find_first(spec => spec.name == normalized) {
Some(spec) => spec.values.copy()
None => []
}
}
///|
pub fn CspBuilder::header_name(self : CspBuilder) -> String {
if self.report_only {
"Content-Security-Policy-Report-Only"
} else {
"Content-Security-Policy"
}
}
///|
pub fn CspDirectiveSpec::render(self : CspDirectiveSpec) -> String {
if self.values.is_empty() {
self.name
} else {
self.name + " " + self.values.join(" ")
}
}
///|
pub fn CspBuilder::render(self : CspBuilder) -> String {
self.directives.map(spec => spec.render()).join("; ")
}
///|
pub fn CspBuilder::to_header_pair(self : CspBuilder) -> HeaderPair {
header_pair(self.header_name(), self.render())
}
///|
pub fn CspBuilder::to_header_line(self : CspBuilder) -> String {
self.to_header_pair().line()
}
///|
pub fn CspBuilder::parse_policy(self : CspBuilder) -> CspPolicy {
parse_csp(self.render())
}
///|
pub fn CspBuilder::analyze(self : CspBuilder) -> CspAnalysis {
analyze_csp_policy(self.parse_policy())
}
///|
pub fn CspBuilder::validate(self : CspBuilder) -> PolicyValidation {
let messages : Array[ValidationMessage] = []
let names : Array[String] = []
for spec in self.directives {
if spec.name.trim().is_empty() {
messages.push(
validation_message(
ValidationError,
"directive",
"CSP directive name cannot be empty.",
),
)
}
if names.any(name => name == spec.name) {
messages.push(
validation_message(
ValidationError,
spec.name,
"CSP directive appears more than once in the builder.",
),
)
} else {
names.push(spec.name)
}
if spec.values.length() > 1 &&
spec.values.any(value => builder_normalize_value(value) == "'none'") {
messages.push(
validation_message(
ValidationWarning,
spec.name,
"'none' should not be combined with other source expressions.",
),
)
}
for value in spec.values {
let source = csp_source_expression(spec.name, value)
if source.kind == CspSourceUnknown {
messages.push(
validation_message(
ValidationNote,
spec.name,
"Source expression `" +
value +
"` is not recognized by the lightweight classifier.",
),
)
}
if !source.secure_transport {
messages.push(
validation_message(
ValidationWarning,
spec.name,
"Source expression `" + value + "` uses insecure transport.",
),
)
}
}
}
if !self.has_directive("default-src") {
messages.push(
validation_message(
ValidationWarning,
"default-src",
"default-src is recommended as a clear fallback.",
),
)
}
{
ok: messages.count_if(item => item.level == ValidationError) == 0,
messages,
}
}
///|
pub fn hsts_config(
max_age : Int,
include_subdomains : Bool,
preload : Bool,
) -> HstsConfig {
{ max_age, include_subdomains, preload }
}
///|
pub fn recommended_hsts_config() -> HstsConfig {
{ max_age: 31536000, include_subdomains: true, preload: false }
}
///|
pub fn HstsConfig::render(self : HstsConfig) -> String {
let parts : Array[String] = ["max-age=" + self.max_age.to_string()]
if self.include_subdomains {
parts.push("includeSubDomains")
}
if self.preload {
parts.push("preload")
}
parts.join("; ")
}
///|
pub fn HstsConfig::to_header_pair(self : HstsConfig) -> HeaderPair {
header_pair("Strict-Transport-Security", self.render())
}
///|
pub fn hsts_header(
max_age : Int,
include_subdomains : Bool,
preload : Bool,
) -> HeaderPair {
hsts_config(max_age, include_subdomains, preload).to_header_pair()
}
///|
pub fn x_content_type_options_header() -> HeaderPair {
header_pair("X-Content-Type-Options", "nosniff")
}
///|
pub fn referrer_policy_header(value : StringView) -> HeaderPair {
header_pair("Referrer-Policy", value)
}
///|
pub fn x_frame_options_header(value : StringView) -> HeaderPair {
header_pair("X-Frame-Options", value)
}
///|
pub fn cross_origin_opener_policy_header(value : StringView) -> HeaderPair {
header_pair("Cross-Origin-Opener-Policy", value)
}
///|
pub fn cross_origin_resource_policy_header(value : StringView) -> HeaderPair {
header_pair("Cross-Origin-Resource-Policy", value)
}
///|
pub fn cross_origin_embedder_policy_header(value : StringView) -> HeaderPair {
header_pair("Cross-Origin-Embedder-Policy", value)
}
///|
pub fn permissions_policy_header(
disabled_features : Array[String],
) -> HeaderPair {
let values : Array[String] = []
for feature in disabled_features {
values.push(feature + "=()")
}
header_pair("Permissions-Policy", values.join(", "))
}
///|
pub fn cache_control_header(value : StringView) -> HeaderPair {
header_pair("Cache-Control", value)
}
///|
pub fn content_disposition_header(value : StringView) -> HeaderPair {
header_pair("Content-Disposition", value)
}
///|
pub fn static_site_csp() -> CspBuilder {
new_csp_builder()
.with_directive("default-src", ["'self'"])
.with_directive("object-src", ["'none'"])
.with_directive("base-uri", ["'self'"])
.with_directive("frame-ancestors", ["'none'"])
.with_directive("script-src", ["'self'"])
.with_directive("style-src", ["'self'"])
.with_directive("img-src", ["'self'", "data:"])
.with_directive("font-src", ["'self'"])
.with_directive("connect-src", ["'self'"])
.with_directive("form-action", ["'self'"])
}
///|
pub fn spa_csp(
api_origin : StringView,
asset_origin : StringView,
) -> CspBuilder {
static_site_csp()
.with_directive("connect-src", ["'self'", api_origin.trim().to_owned()])
.append_value("script-src", asset_origin)
.append_value("style-src", asset_origin)
}
///|
pub fn api_service_csp() -> CspBuilder {
new_csp_builder()
.with_directive("default-src", ["'none'"])
.with_directive("object-src", ["'none'"])
.with_directive("frame-ancestors", ["'none'"])
.with_directive("base-uri", ["'none'"])
.with_directive("form-action", ["'none'"])
}
///|
pub fn admin_console_csp(api_origin : StringView) -> CspBuilder {
new_csp_builder()
.with_directive("default-src", ["'self'"])
.with_directive("script-src", ["'self'"])
.with_directive("style-src", ["'self'"])
.with_directive("connect-src", ["'self'", api_origin.trim().to_owned()])
.with_directive("img-src", ["'self'", "data:"])
.with_directive("object-src", ["'none'"])
.with_directive("base-uri", ["'self'"])
.with_directive("frame-ancestors", ["'none'"])
.with_directive("form-action", ["'self'"])
}
///|
pub fn login_flow_csp(identity_origin : StringView) -> CspBuilder {
new_csp_builder()
.with_directive("default-src", ["'self'"])
.with_directive("script-src", ["'self'"])
.with_directive("style-src", ["'self'"])
.with_directive("connect-src", ["'self'", identity_origin.trim().to_owned()])
.with_directive("img-src", ["'self'", "data:"])
.with_directive("object-src", ["'none'"])
.with_directive("base-uri", ["'self'"])
.with_directive("frame-ancestors", ["'none'"])
.with_directive("form-action", ["'self'", identity_origin.trim().to_owned()])
}
///|
pub fn media_cdn_csp() -> CspBuilder {
new_csp_builder()
.with_directive("default-src", ["'none'"])
.with_directive("img-src", ["'self'", "https:"])
.with_directive("media-src", ["'self'", "https:"])
.with_directive("font-src", ["'self'", "https:"])
.with_directive("object-src", ["'none'"])
.with_directive("base-uri", ["'none'"])
.with_directive("frame-ancestors", ["'none'"])
}
///|
pub fn header_plan(
kind : HeaderPlanKind,
title : StringView,
scenario : StringView,
headers : Array[HeaderPair],
notes : Array[String],
) -> HeaderPlan {
{
kind,
title: title.to_owned(),
scenario: scenario.to_owned(),
headers,
notes,
}
}
///|
pub fn static_site_header_plan() -> HeaderPlan {
header_plan(
HeaderPlanStaticSite,
"Static Site Baseline",
"Public static pages with no privileged browser API access.",
[
static_site_csp().to_header_pair(),
recommended_hsts_config().to_header_pair(),
x_content_type_options_header(),
referrer_policy_header("strict-origin-when-cross-origin"),
permissions_policy_header(["camera", "microphone", "geolocation"]),
cross_origin_opener_policy_header("same-origin"),
cross_origin_resource_policy_header("same-origin"),
],
[
"Designed for documentation, landing pages, and generated static sites.", "If third-party analytics are used, add only the required script and connect origins.",
],
)
}
///|
pub fn spa_header_plan(
api_origin : StringView,
asset_origin : StringView,
) -> HeaderPlan {
header_plan(
HeaderPlanSpaApp,
"SPA Baseline",
"Single-page app shell with explicit API and asset origins.",
[
spa_csp(api_origin, asset_origin).to_header_pair(),
recommended_hsts_config().to_header_pair(),
x_content_type_options_header(),
referrer_policy_header("strict-origin-when-cross-origin"),
permissions_policy_header(["camera", "microphone", "geolocation"]),
cross_origin_opener_policy_header("same-origin"),
cross_origin_resource_policy_header("same-origin"),
],
[
"Keep API and asset origins separate so review output is easy to compare.",
"Use report-only mode first if the current frontend has unknown dynamic imports.",
],
)
}
///|
pub fn api_service_header_plan() -> HeaderPlan {
header_plan(
HeaderPlanApiService,
"API Service Baseline",
"JSON or RPC endpoints that should not expose browser execution surfaces.",
[
api_service_csp().to_header_pair(),
recommended_hsts_config().to_header_pair(),
x_content_type_options_header(),
referrer_policy_header("no-referrer"),
permissions_policy_header([
"camera", "microphone", "geolocation", "payment", "usb",
]),
cross_origin_opener_policy_header("same-origin"),
cross_origin_resource_policy_header("same-origin"),
],
[
"CORS is intentionally not enabled by default.", "Add explicit Access-Control-Allow-Origin only for known browser clients.",
],
)
}
///|
pub fn admin_console_header_plan(api_origin : StringView) -> HeaderPlan {
header_plan(
HeaderPlanAdminConsole,
"Admin Console Baseline",
"Authenticated operational UI with a high-value session surface.",
[
admin_console_csp(api_origin).to_header_pair(),
hsts_header(31536000, true, false),
x_content_type_options_header(),
referrer_policy_header("no-referrer"),
permissions_policy_header([
"camera", "microphone", "geolocation", "payment", "usb",
]),
cross_origin_opener_policy_header("same-origin"),
cross_origin_resource_policy_header("same-origin"),
x_frame_options_header("DENY"),
],
[
"Use no-referrer for administrative paths that may contain tenant or incident identifiers.",
"Keep frame embedding disabled unless there is a documented SSO or support workflow.",
],
)
}
///|
pub fn login_flow_header_plan(identity_origin : StringView) -> HeaderPlan {
header_plan(
HeaderPlanLoginFlow,
"Login Flow Baseline",
"Credential and MFA pages that post to a known identity origin.",
[
login_flow_csp(identity_origin).to_header_pair(),
hsts_header(31536000, true, false),
x_content_type_options_header(),
referrer_policy_header("no-referrer"),
permissions_policy_header(["camera", "microphone", "geolocation"]),
cross_origin_opener_policy_header("same-origin"),
cross_origin_resource_policy_header("same-origin"),
x_frame_options_header("DENY"),
],
[
"Camera or microphone should be opened only for explicit MFA screens.", "Form targets stay limited to the application and identity provider.",
],
)
}
///|
pub fn media_cdn_header_plan() -> HeaderPlan {
header_plan(
HeaderPlanMediaCdn,
"Media CDN Baseline",
"Static asset delivery edge for images, fonts, and media files.",
[
media_cdn_csp().to_header_pair(),
hsts_header(31536000, true, false),
x_content_type_options_header(),
referrer_policy_header("no-referrer"),
permissions_policy_header(["camera", "microphone", "geolocation"]),
cross_origin_resource_policy_header("cross-origin"),
cache_control_header("public, max-age=31536000, immutable"),
],
[
"CDN resources may intentionally be cross-origin embeddable.", "Keep executable surfaces disabled on asset hosts.",
],
)
}
///|
pub fn HeaderPlan::to_header_block(self : HeaderPlan) -> String {
self.headers.map(header => header.line()).join("\n")
}
///|
pub fn HeaderPlan::audit(self : HeaderPlan) -> AuditReport {
audit_headers(self.to_header_block())
}
///|
pub fn HeaderPlan::audit_deep(self : HeaderPlan) -> AuditReport {
audit_headers_with_csp_analysis(self.to_header_block())
}
///|
pub fn HeaderPlan::assess_with_profile(
self : HeaderPlan,
key : StringView,
) -> ProfileAssessment? {
match profile_lookup(key) {
Some(profile) =>
Some(profile.score_headers(parse_headers(self.to_header_block())))
None => None
}
}
///|
pub fn HeaderPlan::validate(self : HeaderPlan) -> PolicyValidation {
let messages : Array[ValidationMessage] = []
let names : Array[String] = []
for header in self.headers {
let normalized = header.normalized_name()
if normalized.is_empty() {
messages.push(
validation_message(
ValidationError,
"header",
"Header name cannot be empty.",
),
)
}
if names.any(name => name == normalized) {
messages.push(
validation_message(
ValidationWarning,
normalized,
"Header appears more than once in the plan.",
),
)
} else {
names.push(normalized)
}
if has_control_character_public(header.name) ||
has_control_character_public(header.value) {
messages.push(
validation_message(
ValidationError,
normalized,
"Header contains a control character.",
),
)
}
if normalized == "content-security-policy" {
let validation = parse_csp(header.value).issues
for issue in validation {
messages.push(
validation_message(
ValidationWarning,
"content-security-policy",
issue.message,
),
)
}
}
}
if !names.any(name => name == "content-security-policy") {
messages.push(
validation_message(
ValidationWarning,
"content-security-policy",
"Header plan does not include an enforcing CSP.",
),
)
}
{
ok: messages.count_if(message => message.level == ValidationError) == 0,
messages,
}
}
///|
pub fn HeaderPlan::to_markdown(self : HeaderPlan) -> String {
let lines : Array[String] = []
lines.push("# " + self.title)
lines.push("")
lines.push("- Kind: `" + self.kind.label() + "`")
lines.push("- Scenario: " + self.scenario)
lines.push("- Headers: " + self.headers.length().to_string())
lines.push("")
lines.push("| Header | Value |")
lines.push("| --- | --- |")
for header in self.headers {
lines.push(header.to_markdown_row())
}
if !self.notes.is_empty() {
lines.push("")
lines.push("## Notes")
for note in self.notes {
lines.push("- " + note)
}
}
lines.join("\n")
}
///|
pub fn HeaderPlan::to_json_string(self : HeaderPlan) -> String {
Json::object(
Map([
("kind", Json::string(self.kind.label())),
("title", Json::string(self.title)),
("scenario", Json::string(self.scenario)),
("headers", Json::array(self.headers.map(header => header_json(header)))),
("notes", Json::array(self.notes.map(note => Json::string(note)))),
]),
).stringify(indent=2)
}
///|
pub fn PolicyValidation::error_count(self : PolicyValidation) -> Int {
self.messages.count_if(message => message.level == ValidationError)
}
///|
pub fn PolicyValidation::warning_count(self : PolicyValidation) -> Int {
self.messages.count_if(message => message.level == ValidationWarning)
}
///|
pub fn PolicyValidation::note_count(self : PolicyValidation) -> Int {
self.messages.count_if(message => message.level == ValidationNote)
}
///|
pub fn PolicyValidation::to_markdown(self : PolicyValidation) -> String {
let lines : Array[String] = []
lines.push("# Policy Validation")
lines.push("")
lines.push("- OK: " + builder_bool_word(self.ok))
lines.push("- Errors: " + self.error_count().to_string())
lines.push("- Warnings: " + self.warning_count().to_string())
lines.push("- Notes: " + self.note_count().to_string())
lines.push("")
for message in self.messages {
lines.push(
"- " +
message.level.label() +
" `" +
message.field +
"`: " +
message.message,
)
}
lines.join("\n")
}
///|
pub fn header_plan_catalog() -> Array[HeaderPlan] {
[
static_site_header_plan(),
spa_header_plan("https://api.example.test", "https://assets.example.test"),
api_service_header_plan(),
admin_console_header_plan("https://api-admin.example.test"),
login_flow_header_plan("https://id.example.test"),
media_cdn_header_plan(),
]
}
///|
pub fn header_plan_keys() -> Array[String] {
header_plan_catalog().map(plan => plan.kind.label())
}
///|
pub fn header_plan_by_key(key : StringView) -> HeaderPlan? {
let expected = key.trim().to_lower().to_owned()
for plan in header_plan_catalog() {
if plan.kind.label() == expected {
return Some(plan)
}
}
None
}
///|
pub fn header_plan_catalog_markdown() -> String {
let lines : Array[String] = []
lines.push("# Header Plan Catalog")
lines.push("")
for plan in header_plan_catalog() {
lines.push("- `" + plan.kind.label() + "` - " + plan.scenario)
}
lines.join("\n")
}
///|
fn validation_message(
level : ValidationLevel,
field : String,
message : String,
) -> ValidationMessage {
{ level, field, message }
}
///|
fn header_json(header : HeaderPair) -> Json {
Json::object(
Map([
("name", Json::string(header.name)),
("value", Json::string(header.value)),
("line", Json::string(header.line())),
]),
)
}
///|
fn builder_normalize_name(name : StringView) -> String {
name.trim().to_lower().to_owned()
}
///|
fn builder_normalize_value(value : StringView) -> String {
value.trim().to_lower().to_owned()
}
///|
fn builder_bool_word(flag : Bool) -> String {
if flag {
"yes"
} else {
"no"
}
}
///|
fn has_control_character_public(value : String) -> Bool {
value.any(ch => {
let code = ch.to_int()
code < 32 && ch != '\t'
})
}