///|
/// Human-facing grade for the policy score.
pub(all) enum PolicyGrade {
GradeExcellent
GradeGood
GradeFair
GradeWeak
GradeDangerous
} derive(Eq, Debug)
///|
/// Priority for a hardening recommendation.
pub(all) enum RecommendationPriority {
PriorityInfo
PriorityLow
PriorityMedium
PriorityHigh
PriorityCritical
} derive(Eq, Debug)
///|
/// Aggregated counts by finding severity.
pub(all) struct SeverityCounts {
info : Int
warning : Int
high : Int
total : Int
} derive(Eq, Debug)
///|
/// Coverage for baseline and optional hardening directives.
pub(all) struct DirectiveCoverage {
required : Int
present : Int
missing : Array[String]
optional_present : Int
unknown : Int
deprecated : Int
experimental : Int
} derive(Eq, Debug)
///|
/// Aggregated counts for classified CSP source expressions.
pub(all) struct SourceCounts {
total : Int
dangerous : Int
network : Int
cryptographic : Int
plain_http : Int
any_origin : Int
data_like : Int
wildcard_host : Int
} derive(Eq, Debug)
///|
/// Full policy score model.
pub(all) struct PolicyScore {
raw_score : Int
final_score : Int
penalty : Int
bonus : Int
grade : PolicyGrade
findings : SeverityCounts
directives : DirectiveCoverage
sources : SourceCounts
} derive(Eq, Debug)
///|
/// Machine-readable hardening suggestion.
pub(all) struct HardeningRecommendation {
code : String
priority : RecommendationPriority
directive : String?
summary : String
detail : String
} derive(Eq, Debug)
///|
pub fn PolicyGrade::name(self : PolicyGrade) -> String {
match self {
GradeExcellent => "excellent"
GradeGood => "good"
GradeFair => "fair"
GradeWeak => "weak"
GradeDangerous => "dangerous"
}
}
///|
pub fn PolicyGrade::label(self : PolicyGrade) -> String {
match self {
GradeExcellent => "A"
GradeGood => "B"
GradeFair => "C"
GradeWeak => "D"
GradeDangerous => "E"
}
}
///|
pub fn RecommendationPriority::name(self : RecommendationPriority) -> String {
match self {
PriorityInfo => "info"
PriorityLow => "low"
PriorityMedium => "medium"
PriorityHigh => "high"
PriorityCritical => "critical"
}
}
///|
pub fn RecommendationPriority::weight(self : RecommendationPriority) -> Int {
match self {
PriorityInfo => 1
PriorityLow => 2
PriorityMedium => 4
PriorityHigh => 8
PriorityCritical => 13
}
}
///|
fn grade_from_score(score : Int) -> PolicyGrade {
if score >= 90 {
GradeExcellent
} else if score >= 75 {
GradeGood
} else if score >= 60 {
GradeFair
} else if score >= 40 {
GradeWeak
} else {
GradeDangerous
}
}
///|
fn clamp_score(score : Int) -> Int {
if score < 0 {
0
} else if score > 100 {
100
} else {
score
}
}
///|
fn count_findings(findings : Array[Finding]) -> SeverityCounts {
let mut info = 0
let mut warning = 0
let mut high = 0
for finding in findings {
if finding.severity == Info {
info = info + 1
} else if finding.severity == Warning {
warning = warning + 1
} else if finding.severity == High {
high = high + 1
}
}
{ info, warning, high, total: findings.length() }
}
///|
pub fn SeverityCounts::blocking(self : SeverityCounts) -> Int {
self.high
}
///|
pub fn SeverityCounts::nonblocking(self : SeverityCounts) -> Int {
self.info + self.warning
}
///|
pub fn SeverityCounts::summary(self : SeverityCounts) -> String {
"total=\{self.total} high=\{self.high} warning=\{self.warning} info=\{self.info}"
}
///|
pub fn Policy::severity_counts(self : Policy) -> SeverityCounts {
count_findings(self.audit())
}
///|
fn required_hardening_names() -> Array[String] {
let names : Array[String] = []
names.push("default-src")
names.push("object-src")
names.push("base-uri")
names.push("frame-ancestors")
names
}
///|
fn optional_hardening_names() -> Array[String] {
let names : Array[String] = []
names.push("form-action")
names.push("upgrade-insecure-requests")
names.push("trusted-types")
names.push("require-trusted-types-for")
names.push("report-to")
names.push("report-uri")
names
}
///|
pub fn Policy::directive_coverage(self : Policy) -> DirectiveCoverage {
let missing : Array[String] = []
let required = required_hardening_names()
let optional = optional_hardening_names()
let mut present = 0
let mut optional_present = 0
for name in required {
if self.contains(name[:]) {
present = present + 1
} else {
missing.push(name)
}
}
for name in optional {
if self.contains(name[:]) {
optional_present = optional_present + 1
}
}
{
required: required.length(),
present,
missing,
optional_present,
unknown: self.unknown_directives().length(),
deprecated: self.deprecated_directives().length(),
experimental: self.experimental_directives().length(),
}
}
///|
pub fn DirectiveCoverage::is_complete(self : DirectiveCoverage) -> Bool {
self.missing.length() == 0
}
///|
pub fn DirectiveCoverage::summary(self : DirectiveCoverage) -> String {
"required=\{self.present}/\{self.required} missing=\{self.missing.length()} optional=\{self.optional_present} unknown=\{self.unknown} deprecated=\{self.deprecated}"
}
///|
pub fn Policy::source_counts(self : Policy) -> SourceCounts {
let sources = self.all_sources()
let mut dangerous = 0
let mut network = 0
let mut cryptographic = 0
let mut plain_http = 0
let mut any_origin = 0
let mut data_like = 0
let mut wildcard_host = 0
for source in sources {
if source.is_dangerous() {
dangerous = dangerous + 1
}
if source.is_network_source() {
network = network + 1
}
if source.is_cryptographic() {
cryptographic = cryptographic + 1
}
if source.uses_plain_http() {
plain_http = plain_http + 1
}
if source.allows_any_origin() {
any_origin = any_origin + 1
}
if source.allows_data_like_url() {
data_like = data_like + 1
}
if source.is_wildcard_host() {
wildcard_host = wildcard_host + 1
}
}
{
total: sources.length(),
dangerous,
network,
cryptographic,
plain_http,
any_origin,
data_like,
wildcard_host,
}
}
///|
pub fn SourceCounts::summary(self : SourceCounts) -> String {
"total=\{self.total} dangerous=\{self.dangerous} network=\{self.network} crypto=\{self.cryptographic} plain_http=\{self.plain_http} any_origin=\{self.any_origin}"
}
///|
pub fn SourceCounts::has_severe_source_risk(self : SourceCounts) -> Bool {
self.plain_http > 0 || self.any_origin > 0 || self.data_like > 0
}
///|
fn score_penalty(
findings : SeverityCounts,
coverage : DirectiveCoverage,
sources : SourceCounts,
) -> Int {
let mut penalty = 0
penalty = penalty + findings.high * 18
penalty = penalty + findings.warning * 7
penalty = penalty + coverage.missing.length() * 9
penalty = penalty + coverage.unknown * 4
penalty = penalty + coverage.deprecated * 6
penalty = penalty + sources.plain_http * 8
penalty = penalty + sources.any_origin * 8
penalty = penalty + sources.data_like * 5
penalty = penalty + sources.wildcard_host * 4
penalty
}
///|
fn score_bonus(
policy : Policy,
coverage : DirectiveCoverage,
sources : SourceCounts,
) -> Int {
let mut bonus = 0
if coverage.is_complete() {
bonus = bonus + 8
}
if policy.contains("upgrade-insecure-requests") {
bonus = bonus + 4
}
if policy.contains("form-action") {
bonus = bonus + 3
}
if policy.contains("report-to") || policy.contains("report-uri") {
bonus = bonus + 2
}
if sources.cryptographic > 0 {
bonus = bonus + 5
}
if policy.contains("trusted-types") {
bonus = bonus + 3
}
if policy.contains("require-trusted-types-for") {
bonus = bonus + 3
}
bonus
}
///|
pub fn Policy::score(self : Policy) -> PolicyScore {
let findings = self.severity_counts()
let coverage = self.directive_coverage()
let sources = self.source_counts()
let penalty = score_penalty(findings, coverage, sources)
let bonus = score_bonus(self, coverage, sources)
let raw_score = 100 - penalty + bonus
let final_score = clamp_score(raw_score)
{
raw_score,
final_score,
penalty,
bonus,
grade: grade_from_score(final_score),
findings,
directives: coverage,
sources,
}
}
///|
pub fn PolicyScore::summary(self : PolicyScore) -> String {
"score=\{self.final_score} grade=\{self.grade.label()} penalty=\{self.penalty} bonus=\{self.bonus}"
}
///|
pub fn PolicyScore::is_passing(self : PolicyScore) -> Bool {
self.final_score >= 75 && self.findings.high == 0
}
///|
pub fn PolicyScore::is_blocked(self : PolicyScore) -> Bool {
self.findings.high > 0 || self.final_score < 40
}
///|
fn make_recommendation(
code : String,
priority : RecommendationPriority,
directive : String?,
summary : String,
detail : String,
) -> HardeningRecommendation {
{ code, priority, directive, summary, detail }
}
///|
fn push_missing_directive_recommendations(
out : Array[HardeningRecommendation],
policy : Policy,
) -> Unit {
if !policy.contains("default-src") {
out.push(
make_recommendation(
"add-default-src",
PriorityCritical,
Some("default-src"),
"Add a default-src baseline.",
"default-src is the fallback for most fetch directives and should be explicit.",
),
)
}
if !policy.contains("object-src") {
out.push(
make_recommendation(
"add-object-src",
PriorityHigh,
Some("object-src"),
"Disable plugin-like object loading.",
"Use object-src 'none' unless the application has a legacy plugin requirement.",
),
)
}
if !policy.contains("base-uri") {
out.push(
make_recommendation(
"add-base-uri",
PriorityHigh,
Some("base-uri"),
"Restrict base URL injection.",
"Use base-uri 'self' or base-uri 'none' to limit attacker-controlled base elements.",
),
)
}
if !policy.contains("frame-ancestors") {
out.push(
make_recommendation(
"add-frame-ancestors",
PriorityHigh,
Some("frame-ancestors"),
"Control who can embed the page.",
"Use frame-ancestors 'none' or an explicit allowlist to reduce clickjacking risk.",
),
)
}
if !policy.contains("form-action") {
out.push(
make_recommendation(
"add-form-action",
PriorityMedium,
Some("form-action"),
"Limit form submission targets.",
"Use form-action 'self' for apps that do not submit forms to third-party origins.",
),
)
}
}
///|
fn push_source_recommendations(
out : Array[HardeningRecommendation],
policy : Policy,
) -> Unit {
if policy.has_source_kind(SourceWildcard) {
out.push(
make_recommendation(
"replace-wildcard",
PriorityCritical,
None,
"Replace wildcard sources.",
"Wildcard sources allow broad loading. Prefer explicit hosts, 'self', or nonce/hash sources.",
),
)
}
if policy.has_source_kind(SourceUnsafeInline) {
out.push(
make_recommendation(
"remove-unsafe-inline",
PriorityCritical,
Some("script-src"),
"Remove unsafe inline script allowance.",
"Use nonces, hashes, or strict-dynamic instead of unsafe-inline where possible.",
),
)
}
if policy.has_source_kind(SourceUnsafeEval) ||
policy.has_source_kind(SourceWasmUnsafeEval) {
out.push(
make_recommendation(
"remove-unsafe-eval",
PriorityCritical,
Some("script-src"),
"Remove dynamic code evaluation.",
"unsafe-eval and wasm-unsafe-eval widen injection impact and should require a documented exception.",
),
)
}
if policy.uses_plain_http() {
out.push(
make_recommendation(
"replace-http",
PriorityHigh,
None,
"Replace plain HTTP sources.",
"Use HTTPS endpoints or upgrade-insecure-requests for transport integrity.",
),
)
}
if policy.has_source_kind(SourceData) {
out.push(
make_recommendation(
"limit-data-url",
PriorityHigh,
None,
"Avoid data: in sensitive directives.",
"data: should be limited to low-risk image or media use cases and avoided for scripts.",
),
)
}
if !policy.uses_nonce_or_hash() && policy.contains("script-src") {
out.push(
make_recommendation(
"add-script-nonce-or-hash",
PriorityMedium,
Some("script-src"),
"Consider nonce or hash based script trust.",
"Nonce or hash sources improve script allowlist precision for modern applications.",
),
)
}
}
///|
fn push_catalog_recommendations(
out : Array[HardeningRecommendation],
policy : Policy,
) -> Unit {
for directive in policy.unknown_directives() {
out.push(
make_recommendation(
"review-unknown-directive",
PriorityMedium,
Some(directive.name),
"Review unknown directive.",
"Unknown directives may be misspelled or unsupported by this catalog.",
),
)
}
for directive in policy.deprecated_directives() {
out.push(
make_recommendation(
"replace-deprecated-directive",
PriorityMedium,
Some(directive.name),
"Replace deprecated directive.",
"Deprecated CSP directives usually have modern alternatives or are ignored by browsers.",
),
)
}
if !policy.contains("upgrade-insecure-requests") && policy.uses_plain_http() {
out.push(
make_recommendation(
"add-upgrade-insecure-requests",
PriorityMedium,
Some("upgrade-insecure-requests"),
"Add upgrade-insecure-requests.",
"This helps migrate old HTTP subresource URLs to HTTPS where supported.",
),
)
}
}
///|
pub fn Policy::hardening_recommendations(
self : Policy,
) -> Array[HardeningRecommendation] {
let out : Array[HardeningRecommendation] = []
push_missing_directive_recommendations(out, self)
push_source_recommendations(out, self)
push_catalog_recommendations(out, self)
out
}
///|
pub fn HardeningRecommendation::summary_line(
self : HardeningRecommendation,
) -> String {
let directive = match self.directive {
Some(name) => name
None => "-"
}
self.priority.name() + " " + self.code + " " + directive + " " + self.summary
}
///|
pub fn HardeningRecommendation::detail_line(
self : HardeningRecommendation,
) -> String {
self.summary_line() + " " + self.detail
}
///|
pub fn recommendations_by_priority(
items : Array[HardeningRecommendation],
priority : RecommendationPriority,
) -> Array[HardeningRecommendation] {
items.filter(item => item.priority == priority)
}
///|
pub fn recommendation_weight_total(
items : Array[HardeningRecommendation],
) -> Int {
let mut total = 0
for item in items {
total = total + item.priority.weight()
}
total
}
///|
pub fn Policy::recommendation_summary(self : Policy) -> String {
let items = self.hardening_recommendations()
let critical = recommendations_by_priority(items, PriorityCritical)
let high = recommendations_by_priority(items, PriorityHigh)
let medium = recommendations_by_priority(items, PriorityMedium)
let low = recommendations_by_priority(items, PriorityLow)
let info = recommendations_by_priority(items, PriorityInfo)
"total=\{items.length()} critical=\{critical.length()} high=\{high.length()} medium=\{medium.length()} low=\{low.length()} info=\{info.length()}"
}