///|
pub fn analyze_domain(
domain : String,
spf_text : String,
dmarc_text : String,
) -> MailPolicyReport {
let spf = parse_spf(spf_text)
let dmarc = parse_dmarc(dmarc_text)
let findings = Array::new(
capacity=spf.findings.length() + dmarc.findings.length() + 1,
)
if domain.trim().is_empty() {
findings.push(
fail(
"domain.name", "Domain name is missing", "Reports should identify the domain being assessed.",
"", "Pass a stable domain label to analyze_domain.",
),
)
} else {
findings.push(pass("domain.name", "Domain name is present", domain))
}
for item in spf.findings {
findings.push(item)
}
for item in dmarc.findings {
findings.push(item)
}
let score = score_findings(findings)
{
domain,
spf,
dmarc,
score,
verdict: verdict_findings(findings, score),
findings,
}
}
///|
pub fn report_markdown(
domain : String,
spf_text : String,
dmarc_text : String,
) -> String {
analyze_domain(domain, spf_text, dmarc_text).to_markdown()
}
///|
pub fn report_json(
domain : String,
spf_text : String,
dmarc_text : String,
) -> String {
analyze_domain(domain, spf_text, dmarc_text).to_json()
}
///|
pub fn MailPolicyReport::has_failures(self : MailPolicyReport) -> Bool {
for finding in self.findings {
if finding.severity == Fail {
return true
}
}
false
}
///|
pub fn MailPolicyReport::warning_count(self : MailPolicyReport) -> Int {
let mut count = 0
for finding in self.findings {
if finding.severity == Warn {
count += 1
}
}
count
}
///|
pub fn MailPolicyReport::to_markdown(self : MailPolicyReport) -> String {
let out = StringBuilder()
out.write_string("# MailShield Report\n\n")
out.write_string("- Domain: " + self.domain + "\n")
out.write_string("- Verdict: " + self.verdict + "\n")
out.write_string("- Score: " + self.score.to_string() + "/100\n")
out.write_string(
"- SPF DNS lookups: " + self.spf.dns_lookup_count.to_string() + "\n",
)
out.write_string("- DMARC policy: " + self.dmarc.policy + "\n\n")
out.write_string("## Findings\n\n")
for item in self.findings {
out.write_string("- [" + severity_text(item.severity) + "] ")
out.write_string(item.id + ": " + item.title)
if item.evidence != "" {
out.write_string(" -- " + item.evidence)
}
if item.severity != Pass && item.repair != "" {
out.write_string(" | repair: " + item.repair)
}
out.write_string("\n")
}
out.to_string()
}
///|
pub fn MailPolicyReport::to_json(self : MailPolicyReport) -> String {
let out = StringBuilder()
out.write_string("{")
out.write_string("\"domain\":\"" + json_escape(self.domain) + "\",")
out.write_string("\"verdict\":\"" + json_escape(self.verdict) + "\",")
out.write_string("\"score\":" + self.score.to_string() + ",")
out.write_string(
"\"spf_dns_lookup_count\":" + self.spf.dns_lookup_count.to_string() + ",",
)
out.write_string(
"\"dmarc_policy\":\"" + json_escape(self.dmarc.policy) + "\",",
)
out.write_string("\"findings\":[")
for i in 0.. 0 {
out.write_string(",")
}
out.write_string("{")
out.write_string("\"id\":\"" + json_escape(item.id) + "\",")
out.write_string("\"severity\":\"" + severity_text(item.severity) + "\",")
out.write_string("\"title\":\"" + json_escape(item.title) + "\"")
out.write_string("}")
}
out.write_string("]}")
out.to_string()
}
///|
fn score_findings(findings : Array[PolicyFinding]) -> Int {
let mut score = 100
for item in findings {
score -= item.penalty
}
clamp_score(score)
}
///|
fn verdict_findings(findings : Array[PolicyFinding], score : Int) -> String {
let mut has_fail = false
let mut has_warn = false
for item in findings {
if item.severity == Fail {
has_fail = true
} else if item.severity == Warn {
has_warn = true
}
}
if has_fail {
"risky"
} else if has_warn || score < 90 {
"monitor"
} else {
"protected"
}
}