///|
/// Output flavor for audit reports.
pub(all) enum ReportFlavor {
ReportText
ReportMarkdown
ReportJson
} derive(Eq, Debug)
///|
pub fn ReportFlavor::name(self : ReportFlavor) -> String {
match self {
ReportText => "text"
ReportMarkdown => "markdown"
ReportJson => "json"
}
}
///|
pub fn Severity::label(self : Severity) -> String {
match self {
Info => "info"
Warning => "warning"
High => "high"
}
}
///|
pub fn Finding::directive_label(self : Finding) -> String {
match self.directive {
Some(name) => name
None => "-"
}
}
///|
/// Escape string content for the JSON reports emitted by CSPKit.
fn json_escape(input : String) -> String {
let out = StringBuilder(size_hint=input.length())
for c in input {
let code = c.to_int()
if code == 34 {
out.write_string("\\\"")
} else if code == 92 {
out.write_string("\\\\")
} else if code == 10 {
out.write_string("\\n")
} else if code == 13 {
out.write_string("\\r")
} else if code == 9 {
out.write_string("\\t")
} else {
out.write_char(c)
}
}
out.to_string()
}
///|
pub fn Finding::text_line(self : Finding) -> String {
self.severity.label() +
" " +
self.code +
" " +
self.directive_label() +
" " +
self.message
}
///|
pub fn Finding::markdown_line(self : Finding) -> String {
"- `" +
self.severity.label() +
"` `" +
self.code +
"` `" +
self.directive_label() +
"` " +
self.message
}
///|
pub fn Finding::json_line(self : Finding) -> String {
"{\"severity\":\"" +
self.severity.label() +
"\",\"code\":\"" +
json_escape(self.code) +
"\",\"directive\":\"" +
json_escape(self.directive_label()) +
"\",\"message\":\"" +
json_escape(self.message) +
"\"}"
}
///|
pub fn Directive::json_line(self : Directive) -> String {
"{\"name\":\"" +
json_escape(self.name) +
"\",\"values\":\"" +
json_escape(self.values.join(" ")) +
"\",\"order\":\{self.order}}"
}
///|
pub fn SourceExpression::json_line(self : SourceExpression) -> String {
"{\"raw\":\"" +
json_escape(self.raw) +
"\",\"kind\":\"" +
self.kind.name() +
"\",\"trust\":\"" +
self.trust.name() +
"\"}"
}
///|
pub fn PolicyScore::text_lines(self : PolicyScore) -> Array[String] {
let lines : Array[String] = []
lines.push("score: " + self.summary())
lines.push("findings: " + self.findings.summary())
lines.push("directives: " + self.directives.summary())
lines.push("sources: " + self.sources.summary())
lines
}
///|
pub fn PolicyScore::text_report(self : PolicyScore) -> String {
self.text_lines().join("\n")
}
///|
pub fn PolicyScore::markdown_report(self : PolicyScore) -> String {
let lines : Array[String] = []
lines.push("## CSPKit Score")
lines.push("")
lines.push("- Score: `\{self.final_score}`")
lines.push("- Grade: `\{self.grade.label()}`")
lines.push("- Penalty: `\{self.penalty}`")
lines.push("- Bonus: `\{self.bonus}`")
lines.push("- Findings: `\{self.findings.summary()}`")
lines.push("- Directives: `\{self.directives.summary()}`")
lines.push("- Sources: `\{self.sources.summary()}`")
lines.join("\n")
}
///|
pub fn PolicyScore::json_report(self : PolicyScore) -> String {
"{\"score\":\{self.final_score},\"grade\":\"" +
self.grade.label() +
"\",\"penalty\":\{self.penalty},\"bonus\":\{self.bonus},\"high\":\{self.findings.high},\"warning\":\{self.findings.warning},\"info\":\{self.findings.info}}"
}
///|
pub fn HardeningRecommendation::markdown_line(
self : HardeningRecommendation,
) -> String {
let directive = match self.directive {
Some(name) => name
None => "-"
}
"- `" +
self.priority.name() +
"` `" +
self.code +
"` `" +
directive +
"` " +
self.summary +
" " +
self.detail
}
///|
pub fn HardeningRecommendation::json_line(
self : HardeningRecommendation,
) -> String {
let directive = match self.directive {
Some(name) => name
None => "-"
}
"{\"priority\":\"" +
self.priority.name() +
"\",\"code\":\"" +
json_escape(self.code) +
"\",\"directive\":\"" +
json_escape(directive) +
"\",\"summary\":\"" +
json_escape(self.summary) +
"\"}"
}
///|
pub fn Policy::audit_text_report(self : Policy) -> String {
let lines : Array[String] = []
lines.push("CSPKit Audit Report")
lines.push(self.score().summary())
lines.push(self.directive_catalog_summary())
lines.push(self.source_inventory_report())
lines.push("Findings")
for finding in self.audit() {
lines.push(finding.text_line())
}
lines.push("Recommendations")
for item in self.hardening_recommendations() {
lines.push(item.summary_line())
}
lines.join("\n")
}
///|
pub fn Policy::audit_markdown_report(self : Policy) -> String {
let lines : Array[String] = []
lines.push("# CSPKit Audit Report")
lines.push("")
lines.push(self.score().markdown_report())
lines.push("")
lines.push("## Directive Catalog")
lines.push("")
lines.push("```text")
lines.push(self.directive_catalog_summary())
lines.push("```")
lines.push("")
lines.push("## Source Inventory")
lines.push("")
lines.push("```text")
lines.push(self.source_inventory_report())
lines.push("```")
lines.push("")
lines.push("## Findings")
lines.push("")
let findings = self.audit()
for finding in findings {
lines.push(finding.markdown_line())
}
if findings.length() == 0 {
lines.push("- No findings.")
}
lines.push("")
lines.push("## Recommendations")
lines.push("")
let recommendations = self.hardening_recommendations()
for item in recommendations {
lines.push(item.markdown_line())
}
if recommendations.length() == 0 {
lines.push("- No recommendations.")
}
lines.join("\n")
}
///|
fn join_json_objects(items : Array[String]) -> String {
let mut out = ""
for i in 0.. String {
let finding_rows : Array[String] = []
let directive_rows : Array[String] = []
let recommendation_rows : Array[String] = []
let source_rows : Array[String] = []
for finding in self.audit() {
finding_rows.push(finding.json_line())
}
for directive in self.directives {
directive_rows.push(directive.json_line())
}
for item in self.hardening_recommendations() {
recommendation_rows.push(item.json_line())
}
for source in self.all_sources() {
source_rows.push(source.json_line())
}
"{\"score\":" +
self.score().json_report() +
",\"directives\":[" +
join_json_objects(directive_rows) +
"],\"sources\":[" +
join_json_objects(source_rows) +
"],\"findings\":[" +
join_json_objects(finding_rows) +
"],\"recommendations\":[" +
join_json_objects(recommendation_rows) +
"]}"
}
///|
pub fn Policy::report(self : Policy, flavor : ReportFlavor) -> String {
match flavor {
ReportText => self.audit_text_report()
ReportMarkdown => self.audit_markdown_report()
ReportJson => self.audit_json_report()
}
}
///|
pub fn DirectiveChange::text_line(self : DirectiveChange) -> String {
self.name +
" before=[" +
self.before.join(" ") +
"] after=[" +
self.after.join(" ") +
"]"
}
///|
pub fn PolicyDiff::text_report(self : PolicyDiff) -> String {
let lines : Array[String] = []
lines.push("Policy Diff")
lines.push("added=\{self.added.length()}")
for directive in self.added {
lines.push("+ " + directive.to_header())
}
lines.push("removed=\{self.removed.length()}")
for directive in self.removed {
lines.push("- " + directive.to_header())
}
lines.push("changed=\{self.changed.length()}")
for change in self.changed {
lines.push("~ " + change.text_line())
}
lines.join("\n")
}
///|
pub fn PolicyDiff::markdown_report(self : PolicyDiff) -> String {
let lines : Array[String] = []
lines.push("## Policy Diff")
lines.push("")
lines.push("### Added")
for directive in self.added {
lines.push("- `" + directive.to_header() + "`")
}
if self.added.length() == 0 {
lines.push("- None")
}
lines.push("")
lines.push("### Removed")
for directive in self.removed {
lines.push("- `" + directive.to_header() + "`")
}
if self.removed.length() == 0 {
lines.push("- None")
}
lines.push("")
lines.push("### Changed")
for change in self.changed {
lines.push("- `" + change.text_line() + "`")
}
if self.changed.length() == 0 {
lines.push("- None")
}
lines.join("\n")
}
///|
pub fn PolicyDiff::has_breaking_change(self : PolicyDiff) -> Bool {
for directive in self.removed {
if directive.name == "default-src" ||
directive.name == "script-src" ||
directive.name == "object-src" ||
directive.name == "base-uri" ||
directive.name == "frame-ancestors" {
return true
}
}
for change in self.changed {
if change.name == "script-src" || change.name == "default-src" {
return true
}
}
false
}
///|
pub fn PolicyDiff::change_summary(self : PolicyDiff) -> String {
"added=\{self.added.length()} removed=\{self.removed.length()} changed=\{self.changed.length()} breaking=\{self.has_breaking_change()}"
}