///|
/// Catalog category for a CSPKit audit rule.
pub(all) enum AuditRuleCategory {
RuleBaseline
RuleScript
RuleTransport
RuleEmbedding
RuleLegacy
RuleReporting
RuleSourceSyntax
RuleMaintenance
RuleTrustedTypes
RuleWorker
RuleMedia
RuleNavigation
} derive(Eq, Debug)
///|
/// Static metadata for audit rules and recommendation mapping.
pub(all) struct AuditRuleSpec {
code : String
category : AuditRuleCategory
severity : Severity
directive : String?
trigger : String
remediation : String
rationale : String
} derive(Eq, Debug)
///|
fn make_audit_rule_spec(
code : String,
category : AuditRuleCategory,
severity : Severity,
directive : String?,
trigger : String,
remediation : String,
rationale : String,
) -> AuditRuleSpec {
{ code, category, severity, directive, trigger, remediation, rationale }
}
///|
pub fn AuditRuleCategory::name(self : AuditRuleCategory) -> String {
match self {
RuleBaseline => "baseline"
RuleScript => "script"
RuleTransport => "transport"
RuleEmbedding => "embedding"
RuleLegacy => "legacy"
RuleReporting => "reporting"
RuleSourceSyntax => "source-syntax"
RuleMaintenance => "maintenance"
RuleTrustedTypes => "trusted-types"
RuleWorker => "worker"
RuleMedia => "media"
RuleNavigation => "navigation"
}
}
///|
pub fn AuditRuleSpec::summary(self : AuditRuleSpec) -> String {
let directive = match self.directive {
Some(name) => name
None => "-"
}
self.code +
" " +
self.category.name() +
" " +
self.severity.label() +
" " +
directive
}
///|
pub fn baseline_audit_rule_specs() -> Array[AuditRuleSpec] {
let rules : Array[AuditRuleSpec] = []
rules.push(
make_audit_rule_spec(
"missing-default-src",
RuleBaseline,
High,
Some("default-src"),
"Policy has no default-src directive.",
"Add default-src with a narrow baseline such as 'self' or 'none'.",
"Most fetch directives use default-src as their fallback.",
),
)
rules.push(
make_audit_rule_spec(
"missing-object-src",
RuleBaseline,
Warning,
Some("object-src"),
"Policy has no object-src directive.",
"Add object-src 'none' unless legacy plugin loading is required.",
"Object and embed content expands attack surface in modern applications.",
),
)
rules.push(
make_audit_rule_spec(
"missing-base-uri",
RuleBaseline,
Warning,
Some("base-uri"),
"Policy has no base-uri directive.",
"Add base-uri 'self' or base-uri 'none'.",
"A hostile base element can rewrite relative URL resolution.",
),
)
rules.push(
make_audit_rule_spec(
"missing-frame-ancestors",
RuleEmbedding,
Warning,
Some("frame-ancestors"),
"Policy has no frame-ancestors directive.",
"Add frame-ancestors 'none' or an explicit embedding allowlist.",
"Clickjacking and unwanted embedding are common deployment risks.",
),
)
rules.push(
make_audit_rule_spec(
"missing-form-action",
RuleNavigation,
Warning,
Some("form-action"),
"Policy has no form-action directive.",
"Add form-action 'self' or an explicit target list.",
"Unexpected form submissions can leak data or bypass intended workflows.",
),
)
rules.push(
make_audit_rule_spec(
"missing-worker-src",
RuleWorker,
Info,
Some("worker-src"),
"Policy has no worker-src directive.",
"Add worker-src when the app uses workers or service workers.",
"Worker loading can otherwise inherit a broader fallback than intended.",
),
)
rules.push(
make_audit_rule_spec(
"missing-connect-src",
RuleTransport,
Info,
Some("connect-src"),
"Policy has no connect-src directive.",
"Add connect-src 'self' or a narrow API endpoint list.",
"Network endpoints deserve explicit review in applications.",
),
)
rules.push(
make_audit_rule_spec(
"missing-img-src",
RuleMedia,
Info,
Some("img-src"),
"Policy has no img-src directive.",
"Add img-src with the image origins used by the page.",
"Explicit media directives make default-src stricter without breaking images.",
),
)
rules.push(
make_audit_rule_spec(
"missing-style-src",
RuleSourceSyntax,
Info,
Some("style-src"),
"Policy has no style-src directive.",
"Add style-src with local styles or an explicit stylesheet allowlist.",
"Stylesheet rules often need a different boundary from scripts.",
),
)
rules.push(
make_audit_rule_spec(
"missing-font-src",
RuleMedia,
Info,
Some("font-src"),
"Policy has no font-src directive.",
"Add font-src if the app loads custom fonts.",
"Explicit font policy avoids accidental inheritance from broad defaults.",
),
)
rules
}
///|
pub fn source_audit_rule_specs() -> Array[AuditRuleSpec] {
let rules : Array[AuditRuleSpec] = []
rules.push(
make_audit_rule_spec(
"wildcard-source",
RuleSourceSyntax,
High,
None,
"A directive contains *.",
"Replace the wildcard with explicit origins or safer keywords.",
"Wildcard sources remove much of the protection CSP is meant to provide.",
),
)
rules.push(
make_audit_rule_spec(
"wildcard-host",
RuleSourceSyntax,
Warning,
None,
"A directive contains a wildcard host.",
"Prefer explicit subdomains or split policy by environment.",
"Wildcard hosts can accidentally include untrusted tenants or forgotten services.",
),
)
rules.push(
make_audit_rule_spec(
"none-with-other-sources",
RuleSourceSyntax,
Warning,
None,
"'none' is mixed with other source expressions.",
"Use 'none' alone or remove it.",
"'none' mixed with other values is confusing and often ignored by readers.",
),
)
rules.push(
make_audit_rule_spec(
"unknown-source-token",
RuleSourceSyntax,
Info,
None,
"A source token is not recognized by CSPKit.",
"Review the token spelling and browser support.",
"Unknown source expressions may be typos or new CSP syntax.",
),
)
rules.push(
make_audit_rule_spec(
"unknown-keyword",
RuleSourceSyntax,
Warning,
None,
"A quoted keyword is not recognized.",
"Use a standard CSP keyword or document the extension.",
"Quoted keywords are easy to mistype and may be ignored.",
),
)
rules.push(
make_audit_rule_spec(
"host-with-path",
RuleSourceSyntax,
Info,
None,
"A host source includes a path.",
"Keep path allowlists only when path scoping is intentionally required.",
"Path matching can be subtle and deserves review.",
),
)
rules.push(
make_audit_rule_spec(
"host-with-port",
RuleSourceSyntax,
Info,
None,
"A host source includes a port.",
"Use explicit ports only when deployment truly needs them.",
"Ports in CSP often indicate environment-specific policy.",
),
)
rules.push(
make_audit_rule_spec(
"scheme-wide-source",
RuleSourceSyntax,
Warning,
None,
"A directive allows a full scheme.",
"Prefer explicit host sources.",
"Scheme-wide allowlists can include too many origins.",
),
)
rules.push(
make_audit_rule_spec(
"broad-https-scheme",
RuleSourceSyntax,
Warning,
None,
"A directive uses https: as a scheme-wide allowlist.",
"Prefer explicit host sources when possible.",
"Scheme-wide trust can include origins outside the application's control.",
),
)
rules.push(
make_audit_rule_spec(
"broad-http-scheme",
RuleTransport,
High,
None,
"A directive uses http: as a scheme-wide allowlist.",
"Use HTTPS and explicit origins.",
"Plain HTTP plus scheme-wide trust is a severe transport weakness.",
),
)
rules.push(
make_audit_rule_spec(
"data-source-sensitive-context",
RuleSourceSyntax,
High,
None,
"data: appears in script, object, or default sources.",
"Avoid data: for sensitive directives.",
"data: URLs are difficult to reason about and can carry executable content.",
),
)
rules.push(
make_audit_rule_spec(
"blob-source",
RuleSourceSyntax,
Warning,
None,
"A directive allows blob: URLs.",
"Limit blob: to image, media, or worker contexts that need it.",
"Blob URLs can hide provenance when used broadly.",
),
)
rules.push(
make_audit_rule_spec(
"filesystem-source",
RuleSourceSyntax,
Warning,
None,
"A directive allows filesystem:.",
"Remove filesystem: unless supporting a known legacy environment.",
"filesystem: is rarely needed and difficult to audit.",
),
)
rules.push(
make_audit_rule_spec(
"mediastream-source",
RuleMedia,
Warning,
None,
"A directive allows mediastream:.",
"Limit mediastream: to media contexts that require it.",
"Media stream URLs should not be allowed broadly.",
),
)
rules
}
///|
pub fn script_audit_rule_specs() -> Array[AuditRuleSpec] {
let rules : Array[AuditRuleSpec] = []
rules.push(
make_audit_rule_spec(
"unsafe-inline",
RuleScript,
High,
Some("script-src"),
"script-src contains unsafe-inline.",
"Use nonces or hashes and remove unsafe-inline.",
"Inline script permissions make injection bugs easier to exploit.",
),
)
rules.push(
make_audit_rule_spec(
"unsafe-eval",
RuleScript,
High,
Some("script-src"),
"Script policy allows dynamic evaluation.",
"Remove unsafe-eval or document a narrow runtime exception.",
"Dynamic evaluation turns string injection into code execution.",
),
)
rules.push(
make_audit_rule_spec(
"wasm-unsafe-eval",
RuleScript,
Warning,
Some("script-src"),
"Script policy allows dynamic WebAssembly compilation.",
"Allow wasm-unsafe-eval only when required by the runtime.",
"Dynamic wasm compilation broadens the executable surface.",
),
)
rules.push(
make_audit_rule_spec(
"script-without-nonce-or-hash",
RuleScript,
Info,
Some("script-src"),
"script-src has no nonce or hash source.",
"Consider nonce or hash based trust for strict deployments.",
"Host-only script policies can be bypassed through trusted origin compromise.",
),
)
rules.push(
make_audit_rule_spec(
"strict-source-token",
RuleScript,
Info,
None,
"A nonce or hash source is present.",
"Keep nonce/hash values unpredictable and deployment-specific.",
"Cryptographic source tokens are a positive signal but still need careful operations.",
),
)
rules.push(
make_audit_rule_spec(
"strict-dynamic-without-nonce",
RuleScript,
Warning,
Some("script-src"),
"strict-dynamic appears without nonce or hash trust.",
"Pair strict-dynamic with a nonce or hash source.",
"strict-dynamic is useful only when a trusted script root exists.",
),
)
rules.push(
make_audit_rule_spec(
"script-blob-source",
RuleScript,
High,
Some("script-src"),
"script-src allows blob:.",
"Avoid blob: in script-src unless a documented worker pattern requires it.",
"Blob scripts can weaken review of executable content provenance.",
),
)
rules.push(
make_audit_rule_spec(
"script-data-source",
RuleScript,
High,
Some("script-src"),
"script-src allows data:.",
"Remove data: from script-src.",
"data: scripts are hard to audit and should be treated as severe risk.",
),
)
rules.push(
make_audit_rule_spec(
"script-http-source",
RuleTransport,
High,
Some("script-src"),
"script-src allows plain HTTP.",
"Use HTTPS script endpoints.",
"Plain HTTP script can be modified in transit.",
),
)
rules.push(
make_audit_rule_spec(
"script-host-allowlist",
RuleScript,
Info,
Some("script-src"),
"script-src uses host allowlists.",
"Review every script host for ownership and supply chain risk.",
"Trusted script origins become part of the application security boundary.",
),
)
rules
}
///|
pub fn transport_audit_rule_specs() -> Array[AuditRuleSpec] {
let rules : Array[AuditRuleSpec] = []
rules.push(
make_audit_rule_spec(
"insecure-http-source",
RuleTransport,
Warning,
None,
"A source uses http: or http://.",
"Use HTTPS endpoints or upgrade-insecure-requests.",
"Plain HTTP can be modified in transit.",
),
)
rules.push(
make_audit_rule_spec(
"missing-upgrade-insecure-requests",
RuleTransport,
Info,
Some("upgrade-insecure-requests"),
"Policy does not request automatic HTTPS upgrade.",
"Add upgrade-insecure-requests for HTTPS-only deployments.",
"The directive helps migrate stale HTTP subresource references.",
),
)
rules.push(
make_audit_rule_spec(
"connect-src-any-origin",
RuleTransport,
High,
Some("connect-src"),
"connect-src allows any origin.",
"Limit connect-src to API, telemetry, and websocket endpoints.",
"Broad network access can leak data or hide malicious exfiltration.",
),
)
rules.push(
make_audit_rule_spec(
"connect-src-http",
RuleTransport,
Warning,
Some("connect-src"),
"connect-src uses plain HTTP.",
"Move API endpoints to HTTPS.",
"Plain HTTP APIs can be read or modified in transit.",
),
)
rules.push(
make_audit_rule_spec(
"websocket-broad",
RuleTransport,
Warning,
Some("connect-src"),
"connect-src allows a broad websocket scheme.",
"Prefer explicit wss:// endpoints.",
"Broad websocket trust weakens network boundary review.",
),
)
rules.push(
make_audit_rule_spec(
"reporting-http",
RuleTransport,
Warning,
Some("report-uri"),
"Violation reports are sent over HTTP.",
"Use HTTPS reporting endpoints.",
"Violation reports may contain sensitive paths and samples.",
),
)
rules
}
///|
pub fn legacy_audit_rule_specs() -> Array[AuditRuleSpec] {
let rules : Array[AuditRuleSpec] = []
rules.push(
make_audit_rule_spec(
"deprecated-directive",
RuleLegacy,
Warning,
None,
"Policy contains a deprecated directive.",
"Remove or replace deprecated directives.",
"Deprecated directives can give a false sense of protection.",
),
)
rules.push(
make_audit_rule_spec(
"plugin-types",
RuleLegacy,
Warning,
Some("plugin-types"),
"Policy uses plugin-types.",
"Prefer object-src 'none' and avoid legacy plugins.",
"Plugin controls are less relevant for modern browsers.",
),
)
rules.push(
make_audit_rule_spec(
"referrer-directive",
RuleLegacy,
Warning,
Some("referrer"),
"Policy uses the deprecated referrer directive.",
"Use the Referrer-Policy header instead.",
"Referrer behavior moved out of CSP.",
),
)
rules.push(
make_audit_rule_spec(
"reflected-xss",
RuleLegacy,
Warning,
Some("reflected-xss"),
"Policy uses reflected-xss.",
"Remove reflected-xss and rely on modern browser defenses.",
"Legacy XSS filters are removed or ignored in modern browsers.",
),
)
rules.push(
make_audit_rule_spec(
"require-sri-for",
RuleLegacy,
Warning,
Some("require-sri-for"),
"Policy uses require-sri-for.",
"Use build tooling and markup integrity attributes instead.",
"The directive was removed from CSP.",
),
)
rules.push(
make_audit_rule_spec(
"block-all-mixed-content",
RuleLegacy,
Info,
Some("block-all-mixed-content"),
"Policy uses block-all-mixed-content.",
"Prefer upgrade-insecure-requests and HTTPS-only deployment.",
"Modern mixed content handling made this directive less useful.",
),
)
rules
}
///|
pub fn reporting_audit_rule_specs() -> Array[AuditRuleSpec] {
let rules : Array[AuditRuleSpec] = []
rules.push(
make_audit_rule_spec(
"missing-reporting",
RuleReporting,
Info,
Some("report-to"),
"Policy has no reporting endpoint.",
"Add report-to or report-uri when violation telemetry is useful.",
"Reporting makes gradual CSP hardening safer.",
),
)
rules.push(
make_audit_rule_spec(
"report-uri-only",
RuleReporting,
Info,
Some("report-uri"),
"Policy uses only report-uri.",
"Consider report-to when the deployment supports Reporting API.",
"report-to is the modern reporting path, while report-uri remains widely used.",
),
)
rules.push(
make_audit_rule_spec(
"report-to-without-endpoint",
RuleReporting,
Info,
Some("report-to"),
"Policy names a report-to group.",
"Ensure the matching Reporting-Endpoints header is deployed.",
"CSP alone does not define the endpoint group transport details.",
),
)
rules.push(
make_audit_rule_spec(
"report-sample-present",
RuleReporting,
Info,
Some("script-src"),
"Policy requests report-sample.",
"Avoid collecting sensitive snippets in logs.",
"Violation samples can help debugging but may include user data.",
),
)
rules
}
///|
pub fn trusted_types_audit_rule_specs() -> Array[AuditRuleSpec] {
let rules : Array[AuditRuleSpec] = []
rules.push(
make_audit_rule_spec(
"missing-trusted-types",
RuleTrustedTypes,
Info,
Some("trusted-types"),
"Policy has no Trusted Types controls.",
"Consider trusted-types and require-trusted-types-for on script-heavy apps.",
"Trusted Types can reduce DOM injection risk in compatible browsers.",
),
)
rules.push(
make_audit_rule_spec(
"trusted-types-star",
RuleTrustedTypes,
Warning,
Some("trusted-types"),
"trusted-types allows any policy name.",
"List the exact Trusted Types policy names used by the application.",
"A wildcard policy name weakens Trusted Types governance.",
),
)
rules.push(
make_audit_rule_spec(
"trusted-types-none",
RuleTrustedTypes,
Info,
Some("trusted-types"),
"trusted-types is set to 'none'.",
"Ensure the application does not require custom Trusted Types policies.",
"This is strict and may be appropriate for low-script pages.",
),
)
rules.push(
make_audit_rule_spec(
"require-trusted-types-script",
RuleTrustedTypes,
Info,
Some("require-trusted-types-for"),
"Policy requires Trusted Types for script sinks.",
"Keep compatibility tests for supported browsers.",
"This is a strong signal for DOM XSS hardening.",
),
)
rules
}
///|
pub fn extended_audit_rule_specs() -> Array[AuditRuleSpec] {
let rules : Array[AuditRuleSpec] = []
for rule in baseline_audit_rule_specs() {
rules.push(rule)
}
for rule in source_audit_rule_specs() {
rules.push(rule)
}
for rule in script_audit_rule_specs() {
rules.push(rule)
}
for rule in transport_audit_rule_specs() {
rules.push(rule)
}
for rule in legacy_audit_rule_specs() {
rules.push(rule)
}
for rule in reporting_audit_rule_specs() {
rules.push(rule)
}
for rule in trusted_types_audit_rule_specs() {
rules.push(rule)
}
rules
}
///|
pub fn audit_rule_spec_by_code(code : StringView) -> AuditRuleSpec? {
let key = lower_ascii(code)
for rule in extended_audit_rule_specs() {
if rule.code == key {
return Some(rule)
}
}
None
}
///|
pub fn audit_rules_by_category(
category : AuditRuleCategory,
) -> Array[AuditRuleSpec] {
extended_audit_rule_specs().filter(rule => rule.category == category)
}
///|
pub fn audit_rules_by_severity(severity : Severity) -> Array[AuditRuleSpec] {
extended_audit_rule_specs().filter(rule => rule.severity == severity)
}
///|
pub fn audit_rule_catalog_report() -> String {
let rows : Array[String] = []
for rule in extended_audit_rule_specs() {
rows.push(rule.summary() + " " + rule.remediation)
}
rows.join("\n")
}
///|
pub fn Finding::rule_spec(self : Finding) -> AuditRuleSpec? {
audit_rule_spec_by_code(self.code[:])
}
///|
pub fn Finding::remediation(self : Finding) -> String {
match self.rule_spec() {
Some(rule) => rule.remediation
None => "Review the finding and adjust the policy intentionally."
}
}
///|
pub fn Finding::rationale(self : Finding) -> String {
match self.rule_spec() {
Some(rule) => rule.rationale
None => self.message
}
}
///|
pub fn Policy::finding_remediation_report(self : Policy) -> String {
let rows : Array[String] = []
for finding in self.audit() {
rows.push(finding.code + ": " + finding.remediation())
}
rows.join("\n")
}
///|
pub fn audit_rule_catalog_summary() -> String {
let all = extended_audit_rule_specs()
let high = audit_rules_by_severity(High)
let warning = audit_rules_by_severity(Warning)
let info = audit_rules_by_severity(Info)
"rules=\{all.length()} high=\{high.length()} warning=\{warning.length()} info=\{info.length()}"
}