///|
/// Classification for a single CSP source expression.
pub(all) enum SourceExpressionKind {
SourceEmpty
SourceNone
SourceSelf
SourceUnsafeInline
SourceUnsafeEval
SourceWasmUnsafeEval
SourceStrictDynamic
SourceUnsafeHashes
SourceReportSample
SourceNonce
SourceSha256
SourceSha384
SourceSha512
SourceScheme
SourceHost
SourceWildcard
SourceData
SourceBlob
SourceFilesystem
SourceMediastream
SourceHttpsScheme
SourceHttpScheme
SourceUpgradeKeyword
SourceUnknownKeyword
SourceUnknown
} derive(Eq, Debug)
///|
/// Coarse trust bucket used by reports and scoring.
pub(all) enum SourceTrust {
TrustBlocksAll
TrustSameOrigin
TrustCryptographic
TrustExplicitOrigin
TrustTransportScheme
TrustDynamic
TrustDangerous
TrustUnknown
} derive(Eq, Debug)
///|
/// Structured information derived from a raw CSP source expression.
pub(all) struct SourceExpression {
raw : String
normalized : String
kind : SourceExpressionKind
trust : SourceTrust
scheme : String?
host : String?
port : String?
path : String?
description : String
} derive(Eq, Debug)
///|
let source_ch_colon = 58
///|
let source_ch_slash = 47
///|
let source_ch_star = 42
///|
let source_ch_dot = 46
///|
let source_ch_lbracket = 91
///|
let source_ch_rbracket = 93
///|
let source_ch_question = 63
///|
let source_ch_hash = 35
///|
fn char_at_source(input : String, index : Int) -> Int {
input.unsafe_get(index).to_int()
}
///|
fn source_kind_name(kind : SourceExpressionKind) -> String {
match kind {
SourceEmpty => "empty"
SourceNone => "none"
SourceSelf => "self"
SourceUnsafeInline => "unsafe-inline"
SourceUnsafeEval => "unsafe-eval"
SourceWasmUnsafeEval => "wasm-unsafe-eval"
SourceStrictDynamic => "strict-dynamic"
SourceUnsafeHashes => "unsafe-hashes"
SourceReportSample => "report-sample"
SourceNonce => "nonce"
SourceSha256 => "sha256"
SourceSha384 => "sha384"
SourceSha512 => "sha512"
SourceScheme => "scheme"
SourceHost => "host"
SourceWildcard => "wildcard"
SourceData => "data"
SourceBlob => "blob"
SourceFilesystem => "filesystem"
SourceMediastream => "mediastream"
SourceHttpsScheme => "https-scheme"
SourceHttpScheme => "http-scheme"
SourceUpgradeKeyword => "upgrade-keyword"
SourceUnknownKeyword => "unknown-keyword"
SourceUnknown => "unknown"
}
}
///|
pub fn SourceExpressionKind::name(self : SourceExpressionKind) -> String {
source_kind_name(self)
}
///|
pub fn SourceTrust::name(self : SourceTrust) -> String {
match self {
TrustBlocksAll => "blocks-all"
TrustSameOrigin => "same-origin"
TrustCryptographic => "cryptographic"
TrustExplicitOrigin => "explicit-origin"
TrustTransportScheme => "transport-scheme"
TrustDynamic => "dynamic"
TrustDangerous => "dangerous"
TrustUnknown => "unknown"
}
}
///|
fn source_description(kind : SourceExpressionKind) -> String {
match kind {
SourceEmpty => "Empty source expression."
SourceNone => "Blocks every source when used alone."
SourceSelf => "Allows resources from the protected document origin."
SourceUnsafeInline => "Allows inline code and weakens injection protection."
SourceUnsafeEval => "Allows string-to-code evaluation."
SourceWasmUnsafeEval => "Allows dynamic WebAssembly compilation."
SourceStrictDynamic => "Delegates script trust to nonced or hashed scripts."
SourceUnsafeHashes => "Allows inline event handlers matched by hashes."
SourceReportSample => "Requests a source sample in violation reports."
SourceNonce => "Cryptographic nonce source."
SourceSha256 => "SHA-256 hash source."
SourceSha384 => "SHA-384 hash source."
SourceSha512 => "SHA-512 hash source."
SourceScheme => "Allows every origin using a scheme."
SourceHost => "Allows an explicit host source."
SourceWildcard => "Allows any network source."
SourceData => "Allows data: URLs."
SourceBlob => "Allows blob: URLs."
SourceFilesystem => "Allows filesystem: URLs."
SourceMediastream => "Allows mediastream: URLs."
SourceHttpsScheme => "Allows any HTTPS origin."
SourceHttpScheme => "Allows any plain HTTP origin."
SourceUpgradeKeyword => "Requests upgrading insecure requests."
SourceUnknownKeyword => "Quoted keyword not recognized by CSPKit."
SourceUnknown => "Source expression could not be classified precisely."
}
}
///|
fn source_trust_for(kind : SourceExpressionKind) -> SourceTrust {
match kind {
SourceNone => TrustBlocksAll
SourceSelf => TrustSameOrigin
SourceNonce => TrustCryptographic
SourceSha256 => TrustCryptographic
SourceSha384 => TrustCryptographic
SourceSha512 => TrustCryptographic
SourceHost => TrustExplicitOrigin
SourceHttpsScheme => TrustTransportScheme
SourceScheme => TrustTransportScheme
SourceStrictDynamic => TrustDynamic
SourceReportSample => TrustDynamic
SourceUpgradeKeyword => TrustDynamic
SourceWildcard => TrustDangerous
SourceUnsafeInline => TrustDangerous
SourceUnsafeEval => TrustDangerous
SourceWasmUnsafeEval => TrustDangerous
SourceUnsafeHashes => TrustDangerous
SourceData => TrustDangerous
SourceBlob => TrustDangerous
SourceFilesystem => TrustDangerous
SourceMediastream => TrustDangerous
SourceHttpScheme => TrustDangerous
SourceEmpty => TrustUnknown
SourceUnknownKeyword => TrustUnknown
SourceUnknown => TrustUnknown
}
}
///|
fn index_of_code(input : String, code : Int) -> Int {
for i in 0.. Int {
if start < 0 {
return index_of_code(input, code)
}
for i in start.. Bool {
index_of_code(input, code) >= 0
}
///|
fn ends_with_text(input : String, suffix : String) -> Bool {
suffix.length() <= input.length() &&
input[input.length() - suffix.length():input.length()].to_owned() == suffix
}
///|
fn contains_text(input : String, needle : String) -> Bool {
if needle.length() == 0 {
return true
}
if needle.length() > input.length() {
return false
}
for i in 0..<(input.length() - needle.length() + 1) {
if input[i:i + needle.length()].to_owned() == needle {
return true
}
}
false
}
///|
fn before_query_or_hash(input : String) -> String {
let query = index_of_code(input, source_ch_question)
let hash = index_of_code(input, source_ch_hash)
if query >= 0 && hash >= 0 {
let end = if query < hash { query } else { hash }
input[0:end].to_owned()
} else if query >= 0 {
input[0:query].to_owned()
} else if hash >= 0 {
input[0:hash].to_owned()
} else {
input
}
}
///|
fn scheme_part(value : String) -> String? {
let colon = index_of_code(value, source_ch_colon)
if colon <= 0 {
None
} else {
Some(value[0:colon].to_owned())
}
}
///|
fn value_after_scheme(value : String) -> String {
let colon = index_of_code(value, source_ch_colon)
if colon < 0 || colon + 1 >= value.length() {
""
} else {
value[colon + 1:value.length()].to_owned()
}
}
///|
pub fn source_scheme_tail(raw : StringView) -> String {
value_after_scheme(trim_csp(raw))
}
///|
fn has_scheme_separator(value : String) -> Bool {
let colon = index_of_code(value, source_ch_colon)
colon >= 0 &&
colon + 2 < value.length() &&
char_at_source(value, colon + 1) == source_ch_slash &&
char_at_source(value, colon + 2) == source_ch_slash
}
///|
fn path_start_after_authority(value : String) -> Int {
let colon = index_of_code(value, source_ch_colon)
if colon >= 0 &&
colon + 2 < value.length() &&
char_at_source(value, colon + 1) == source_ch_slash &&
char_at_source(value, colon + 2) == source_ch_slash {
let slash = index_of_code_from(value, source_ch_slash, colon + 3)
slash
} else {
index_of_code(value, source_ch_slash)
}
}
///|
fn authority_part(value : String) -> String {
let clean = before_query_or_hash(value)
let start = if has_scheme_separator(clean) {
index_of_code(clean, source_ch_colon) + 3
} else {
0
}
let slash = index_of_code_from(clean, source_ch_slash, start)
if slash >= 0 {
clean[start:slash].to_owned()
} else {
clean[start:clean.length()].to_owned()
}
}
///|
fn path_part(value : String) -> String? {
let clean = before_query_or_hash(value)
let start = path_start_after_authority(clean)
if start >= 0 && start < clean.length() {
Some(clean[start:clean.length()].to_owned())
} else {
None
}
}
///|
fn split_host_port(authority : String) -> (String?, String?) {
if authority.length() == 0 {
return (None, None)
}
if char_at_source(authority, 0) == source_ch_lbracket {
let close = index_of_code(authority, source_ch_rbracket)
if close > 0 {
let host = authority[0:close + 1].to_owned()
if close + 2 < authority.length() &&
char_at_source(authority, close + 1) == source_ch_colon {
return (
Some(host),
Some(authority[close + 2:authority.length()].to_owned()),
)
}
return (Some(host), None)
}
}
let colon = index_of_code(authority, source_ch_colon)
if colon > 0 {
(
Some(authority[0:colon].to_owned()),
Some(authority[colon + 1:authority.length()].to_owned()),
)
} else {
(Some(authority), None)
}
}
///|
fn quoted_keyword_kind(lower : String) -> SourceExpressionKind? {
if lower == "'none'" {
Some(SourceNone)
} else if lower == "'self'" {
Some(SourceSelf)
} else if lower == "'unsafe-inline'" {
Some(SourceUnsafeInline)
} else if lower == "'unsafe-eval'" {
Some(SourceUnsafeEval)
} else if lower == "'wasm-unsafe-eval'" {
Some(SourceWasmUnsafeEval)
} else if lower == "'strict-dynamic'" {
Some(SourceStrictDynamic)
} else if lower == "'unsafe-hashes'" {
Some(SourceUnsafeHashes)
} else if lower == "'report-sample'" {
Some(SourceReportSample)
} else {
None
}
}
///|
fn hash_kind(lower : String) -> SourceExpressionKind? {
if starts_with(lower, "'sha256-") && ends_with_text(lower, "'") {
Some(SourceSha256)
} else if starts_with(lower, "'sha384-") && ends_with_text(lower, "'") {
Some(SourceSha384)
} else if starts_with(lower, "'sha512-") && ends_with_text(lower, "'") {
Some(SourceSha512)
} else {
None
}
}
///|
fn scheme_only_kind(lower : String) -> SourceExpressionKind? {
if lower == "http:" {
Some(SourceHttpScheme)
} else if lower == "https:" {
Some(SourceHttpsScheme)
} else if lower == "data:" {
Some(SourceData)
} else if lower == "blob:" {
Some(SourceBlob)
} else if lower == "filesystem:" {
Some(SourceFilesystem)
} else if lower == "mediastream:" {
Some(SourceMediastream)
} else if ends_with_text(lower, ":") {
Some(SourceScheme)
} else {
None
}
}
///|
fn classify_source_kind(raw : String, lower : String) -> SourceExpressionKind {
if raw.length() == 0 {
return SourceEmpty
}
if raw.length() == 1 && char_at_source(raw, 0) == source_ch_star {
return SourceWildcard
}
match quoted_keyword_kind(lower) {
Some(kind) => return kind
None => ()
}
if starts_with(lower, "'nonce-") && ends_with_text(lower, "'") {
return SourceNonce
}
match hash_kind(lower) {
Some(kind) => return kind
None => ()
}
match scheme_only_kind(lower) {
Some(kind) => return kind
None => ()
}
if lower == "upgrade-insecure-requests" {
return SourceUpgradeKeyword
}
if starts_with(lower, "'") && ends_with_text(lower, "'") {
return SourceUnknownKeyword
}
if has_scheme_separator(lower) ||
contains_code(lower, source_ch_dot) ||
starts_with(lower, "*.") {
return SourceHost
}
SourceUnknown
}
///|
pub fn classify_source_expression(raw : StringView) -> SourceExpression {
let value = trim_csp(raw)
let lower = lower_ascii(value[:])
let kind = classify_source_kind(value, lower)
let scheme = scheme_part(lower)
let authority = authority_part(value)
let (host, port) = split_host_port(authority)
let path = path_part(value)
let trust = source_trust_for(kind)
let description = source_description(kind)
{
raw: value,
normalized: lower,
kind,
trust,
scheme,
host,
port,
path,
description,
}
}
///|
pub fn SourceExpression::is_empty(self : SourceExpression) -> Bool {
self.kind == SourceEmpty
}
///|
pub fn SourceExpression::is_keyword(self : SourceExpression) -> Bool {
self.kind == SourceNone ||
self.kind == SourceSelf ||
self.kind == SourceUnsafeInline ||
self.kind == SourceUnsafeEval ||
self.kind == SourceWasmUnsafeEval ||
self.kind == SourceStrictDynamic ||
self.kind == SourceUnsafeHashes ||
self.kind == SourceReportSample
}
///|
pub fn SourceExpression::is_cryptographic(self : SourceExpression) -> Bool {
self.kind == SourceNonce ||
self.kind == SourceSha256 ||
self.kind == SourceSha384 ||
self.kind == SourceSha512
}
///|
pub fn SourceExpression::is_network_source(self : SourceExpression) -> Bool {
self.kind == SourceHost ||
self.kind == SourceScheme ||
self.kind == SourceHttpsScheme ||
self.kind == SourceHttpScheme ||
self.kind == SourceWildcard
}
///|
pub fn SourceExpression::is_local_like(self : SourceExpression) -> Bool {
self.kind == SourceSelf || self.trust == TrustCryptographic
}
///|
pub fn SourceExpression::is_dangerous(self : SourceExpression) -> Bool {
self.trust == TrustDangerous
}
///|
pub fn SourceExpression::is_dynamic(self : SourceExpression) -> Bool {
self.trust == TrustDynamic
}
///|
pub fn SourceExpression::uses_plain_http(self : SourceExpression) -> Bool {
self.kind == SourceHttpScheme ||
self.scheme is Some("http") ||
starts_with(self.normalized, "http://")
}
///|
pub fn SourceExpression::allows_any_origin(self : SourceExpression) -> Bool {
self.kind == SourceWildcard ||
self.normalized == "https:" ||
self.normalized == "http:" ||
self.normalized == "*:"
}
///|
pub fn SourceExpression::allows_data_like_url(self : SourceExpression) -> Bool {
self.kind == SourceData ||
self.kind == SourceBlob ||
self.kind == SourceFilesystem ||
self.kind == SourceMediastream
}
///|
pub fn SourceExpression::is_wildcard_host(self : SourceExpression) -> Bool {
starts_with(self.normalized, "*.") || contains_text(self.normalized, "://*.")
}
///|
pub fn SourceExpression::is_host_with_path(self : SourceExpression) -> Bool {
self.kind == SourceHost && self.path is Some(_)
}
///|
pub fn SourceExpression::has_port(self : SourceExpression) -> Bool {
self.port is Some(_)
}
///|
pub fn SourceExpression::has_scheme(self : SourceExpression) -> Bool {
self.scheme is Some(_)
}
///|
pub fn SourceExpression::summary(self : SourceExpression) -> String {
self.raw + " kind=" + self.kind.name() + " trust=" + self.trust.name()
}
///|
pub fn SourceExpression::detail(self : SourceExpression) -> String {
let scheme = match self.scheme {
Some(scheme) => scheme
None => "-"
}
let host = match self.host {
Some(host) => host
None => "-"
}
let port = match self.port {
Some(port) => port
None => "-"
}
let path = match self.path {
Some(path) => path
None => "-"
}
self.summary() +
" scheme=" +
scheme +
" host=" +
host +
" port=" +
port +
" path=" +
path
}
///|
pub fn Directive::source_expressions(
self : Directive,
) -> Array[SourceExpression] {
self.values.map(value => classify_source_expression(value[:]))
}
///|
pub fn Directive::dangerous_sources(
self : Directive,
) -> Array[SourceExpression] {
self.source_expressions().filter(source => source.is_dangerous())
}
///|
pub fn Directive::network_sources(self : Directive) -> Array[SourceExpression] {
self.source_expressions().filter(source => source.is_network_source())
}
///|
pub fn Directive::cryptographic_sources(
self : Directive,
) -> Array[SourceExpression] {
self.source_expressions().filter(source => source.is_cryptographic())
}
///|
pub fn Directive::has_source_kind(
self : Directive,
kind : SourceExpressionKind,
) -> Bool {
for source in self.source_expressions() {
if source.kind == kind {
return true
}
}
false
}
///|
pub fn Directive::has_dangerous_source(self : Directive) -> Bool {
for source in self.source_expressions() {
if source.is_dangerous() {
return true
}
}
false
}
///|
pub fn Directive::has_cryptographic_source(self : Directive) -> Bool {
for source in self.source_expressions() {
if source.is_cryptographic() {
return true
}
}
false
}
///|
pub fn Directive::has_network_source(self : Directive) -> Bool {
for source in self.source_expressions() {
if source.is_network_source() {
return true
}
}
false
}
///|
pub fn Directive::source_summary(self : Directive) -> String {
let rows : Array[String] = []
for source in self.source_expressions() {
rows.push(source.summary())
}
rows.join("\n")
}
///|
pub fn Policy::all_sources(self : Policy) -> Array[SourceExpression] {
let out : Array[SourceExpression] = []
for directive in self.directives {
for source in directive.source_expressions() {
out.push(source)
}
}
out
}
///|
pub fn Policy::dangerous_sources(self : Policy) -> Array[SourceExpression] {
self.all_sources().filter(source => source.is_dangerous())
}
///|
pub fn Policy::cryptographic_sources(self : Policy) -> Array[SourceExpression] {
self.all_sources().filter(source => source.is_cryptographic())
}
///|
pub fn Policy::network_sources(self : Policy) -> Array[SourceExpression] {
self.all_sources().filter(source => source.is_network_source())
}
///|
pub fn Policy::source_kind_count(
self : Policy,
kind : SourceExpressionKind,
) -> Int {
let mut count = 0
for source in self.all_sources() {
if source.kind == kind {
count = count + 1
}
}
count
}
///|
pub fn Policy::has_source_kind(
self : Policy,
kind : SourceExpressionKind,
) -> Bool {
self.source_kind_count(kind) > 0
}
///|
pub fn Policy::uses_nonce_or_hash(self : Policy) -> Bool {
for source in self.all_sources() {
if source.is_cryptographic() {
return true
}
}
false
}
///|
pub fn Policy::uses_plain_http(self : Policy) -> Bool {
for source in self.all_sources() {
if source.uses_plain_http() {
return true
}
}
false
}
///|
pub fn Policy::allows_any_origin(self : Policy) -> Bool {
for source in self.all_sources() {
if source.allows_any_origin() {
return true
}
}
false
}
///|
pub fn Policy::source_inventory_report(self : Policy) -> String {
let lines : Array[String] = []
lines.push("sources=\{self.all_sources().length()}")
lines.push("dangerous=\{self.dangerous_sources().length()}")
lines.push("network=\{self.network_sources().length()}")
lines.push("cryptographic=\{self.cryptographic_sources().length()}")
lines.join("\n")
}