server: TLS report support
This commit is contained in:
parent
8218fca2ef
commit
8f53678e53
279
server/src/nm.rs
279
server/src/nm.rs
@ -1,7 +1,7 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fs::File,
|
||||
io::Cursor,
|
||||
io::{Cursor, Read},
|
||||
};
|
||||
|
||||
use askama::Template;
|
||||
@ -33,6 +33,7 @@ const MESSAGE_RFC822: &'static str = "message/rfc822";
|
||||
const MULTIPART_ALTERNATIVE: &'static str = "multipart/alternative";
|
||||
const MULTIPART_MIXED: &'static str = "multipart/mixed";
|
||||
const MULTIPART_RELATED: &'static str = "multipart/related";
|
||||
const MULTIPART_REPORT: &'static str = "multipart/report";
|
||||
const TEXT_HTML: &'static str = "text/html";
|
||||
const TEXT_PLAIN: &'static str = "text/plain";
|
||||
|
||||
@ -453,6 +454,7 @@ fn extract_body(m: &ParsedMail, part_addr: &mut Vec<String>) -> Result<Body, Ser
|
||||
MULTIPART_MIXED => extract_mixed(m, part_addr),
|
||||
MULTIPART_ALTERNATIVE => extract_alternative(m, part_addr),
|
||||
MULTIPART_RELATED => extract_related(m, part_addr),
|
||||
MULTIPART_REPORT => extract_report(m, part_addr),
|
||||
APPLICATION_ZIP => extract_zip(m),
|
||||
_ => extract_unhandled(m),
|
||||
};
|
||||
@ -528,6 +530,83 @@ fn extract_gzip(m: &ParsedMail) -> Result<Body, ServerError> {
|
||||
extract_unhandled(m)
|
||||
}
|
||||
|
||||
fn extract_report(m: &ParsedMail, _part_addr: &mut Vec<String>) -> Result<Body, ServerError> {
|
||||
let mut html_part = None;
|
||||
let mut tlsrpt_part = None;
|
||||
|
||||
for sp in &m.subparts {
|
||||
match sp.ctype.mimetype.as_str() {
|
||||
TEXT_HTML => html_part = Some(sp.get_body()?),
|
||||
"application/tlsrpt+gzip" => tlsrpt_part = Some(sp.get_body_raw()?),
|
||||
_ => {} // Ignore other parts for now
|
||||
}
|
||||
}
|
||||
|
||||
let tlsrpt_summary_html = if let Some(gz_bytes) = tlsrpt_part {
|
||||
let mut decoder = flate2::read::GzDecoder::new(&gz_bytes[..]);
|
||||
let mut buffer = Vec::new();
|
||||
if decoder.read_to_end(&mut buffer).is_ok() {
|
||||
if let Ok(json_str) = String::from_utf8(buffer) {
|
||||
match serde_json::from_str::<TlsRpt>(&json_str) {
|
||||
Ok(tlsrpt) => {
|
||||
let formatted_tlsrpt = FormattedTlsRpt {
|
||||
organization_name: tlsrpt.organization_name,
|
||||
date_range: FormattedTlsRptDateRange {
|
||||
start_datetime: tlsrpt.date_range.start_datetime,
|
||||
end_datetime: tlsrpt.date_range.end_datetime,
|
||||
},
|
||||
contact_info: tlsrpt.contact_info.unwrap_or_else(|| "".to_string()),
|
||||
report_id: tlsrpt.report_id,
|
||||
policies: tlsrpt.policies.into_iter().map(|policy| {
|
||||
FormattedTlsRptPolicy {
|
||||
policy: FormattedTlsRptPolicyDetails {
|
||||
policy_type: policy.policy.policy_type,
|
||||
policy_string: policy.policy.policy_string,
|
||||
policy_domain: policy.policy.policy_domain,
|
||||
mx_host: policy.policy.mx_host.unwrap_or_else(|| Vec::new()),
|
||||
},
|
||||
summary: policy.summary,
|
||||
failure_details: policy.failure_details.unwrap_or_else(|| Vec::new()).into_iter().map(|detail| {
|
||||
FormattedTlsRptFailureDetails {
|
||||
result_type: detail.result_type,
|
||||
sending_mta_ip: detail.sending_mta_ip.unwrap_or_else(|| "".to_string()),
|
||||
receiving_ip: detail.receiving_ip.unwrap_or_else(|| "".to_string()),
|
||||
receiving_mx_hostname: detail.receiving_mx_hostname.unwrap_or_else(|| "".to_string()),
|
||||
failed_session_count: detail.failed_session_count,
|
||||
additional_info: detail.additional_info.unwrap_or_else(|| "".to_string()),
|
||||
failure_reason_code: detail.failure_reason_code.unwrap_or_else(|| "".to_string()),
|
||||
}
|
||||
}).collect(),
|
||||
}
|
||||
}).collect(),
|
||||
};
|
||||
let template = TlsReportTemplate { report: &formatted_tlsrpt };
|
||||
template.render().unwrap_or_else(|e| format!("<div class=\"tlsrpt-error\">Failed to render TLS report template: {}</div>", e))
|
||||
}
|
||||
Err(e) => format!(
|
||||
"<div class=\"tlsrpt-error\">Failed to parse TLS report JSON: {}</div>",
|
||||
e
|
||||
),
|
||||
}
|
||||
} else {
|
||||
format!("<div class=\"tlsrpt-error\">Failed to convert decompressed data to UTF-8.</div>")
|
||||
}
|
||||
} else {
|
||||
format!("<div class=\"tlsrpt-error\">Failed to decompressed data.</div>")
|
||||
}
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
let final_html = if let Some(html) = html_part {
|
||||
format!("{}<hr>{} ", html, tlsrpt_summary_html)
|
||||
} else {
|
||||
tlsrpt_summary_html
|
||||
};
|
||||
|
||||
Ok(Body::html(final_html))
|
||||
}
|
||||
|
||||
fn extract_unhandled(m: &ParsedMail) -> Result<Body, ServerError> {
|
||||
let msg = format!(
|
||||
"Unhandled body content type:\n{}\n{}",
|
||||
@ -1287,6 +1366,123 @@ pub struct Reason {
|
||||
pub reason_type: Option<String>,
|
||||
pub comment: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct TlsRpt {
|
||||
#[serde(rename = "organization-name")]
|
||||
pub organization_name: String,
|
||||
#[serde(rename = "date-range")]
|
||||
pub date_range: TlsRptDateRange,
|
||||
#[serde(rename = "contact-info")]
|
||||
pub contact_info: Option<String>,
|
||||
#[serde(rename = "report-id")]
|
||||
pub report_id: String,
|
||||
pub policies: Vec<TlsRptPolicy>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct TlsRptDateRange {
|
||||
#[serde(rename = "start-datetime")]
|
||||
pub start_datetime: String,
|
||||
#[serde(rename = "end-datetime")]
|
||||
pub end_datetime: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct TlsRptPolicy {
|
||||
pub policy: TlsRptPolicyDetails,
|
||||
pub summary: TlsRptSummary,
|
||||
#[serde(rename = "failure-details")]
|
||||
pub failure_details: Option<Vec<TlsRptFailureDetails>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct TlsRptPolicyDetails {
|
||||
#[serde(rename = "policy-type")]
|
||||
pub policy_type: String,
|
||||
#[serde(rename = "policy-string")]
|
||||
pub policy_string: Vec<String>,
|
||||
#[serde(rename = "policy-domain")]
|
||||
pub policy_domain: String,
|
||||
#[serde(rename = "mx-host")]
|
||||
pub mx_host: Option<Vec<TlsRptMxHost>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct TlsRptSummary {
|
||||
#[serde(rename = "total-successful-session-count")]
|
||||
pub total_successful_session_count: u64,
|
||||
#[serde(rename = "total-failure-session-count")]
|
||||
pub total_failure_session_count: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct TlsRptMxHost {
|
||||
pub hostname: String,
|
||||
#[serde(rename = "failure-count")]
|
||||
pub failure_count: u64,
|
||||
#[serde(rename = "result-type")]
|
||||
pub result_type: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct TlsRptFailureDetails {
|
||||
#[serde(rename = "result-type")]
|
||||
pub result_type: String,
|
||||
#[serde(rename = "sending-mta-ip")]
|
||||
pub sending_mta_ip: Option<String>,
|
||||
#[serde(rename = "receiving-ip")]
|
||||
pub receiving_ip: Option<String>,
|
||||
#[serde(rename = "receiving-mx-hostname")]
|
||||
pub receiving_mx_hostname: Option<String>,
|
||||
#[serde(rename = "failed-session-count")]
|
||||
pub failed_session_count: u64,
|
||||
#[serde(rename = "additional-info")]
|
||||
pub additional_info: Option<String>,
|
||||
#[serde(rename = "failure-reason-code")]
|
||||
pub failure_reason_code: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FormattedTlsRpt {
|
||||
pub organization_name: String,
|
||||
pub date_range: FormattedTlsRptDateRange,
|
||||
pub contact_info: String,
|
||||
pub report_id: String,
|
||||
pub policies: Vec<FormattedTlsRptPolicy>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FormattedTlsRptDateRange {
|
||||
pub start_datetime: String,
|
||||
pub end_datetime: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FormattedTlsRptPolicy {
|
||||
pub policy: FormattedTlsRptPolicyDetails,
|
||||
pub summary: TlsRptSummary,
|
||||
pub failure_details: Vec<FormattedTlsRptFailureDetails>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FormattedTlsRptPolicyDetails {
|
||||
pub policy_type: String,
|
||||
pub policy_string: Vec<String>,
|
||||
pub policy_domain: String,
|
||||
pub mx_host: Vec<TlsRptMxHost>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FormattedTlsRptFailureDetails {
|
||||
pub result_type: String,
|
||||
pub sending_mta_ip: String,
|
||||
pub receiving_ip: String,
|
||||
pub receiving_mx_hostname: String,
|
||||
pub failed_session_count: u64,
|
||||
pub additional_info: String,
|
||||
pub failure_reason_code: String,
|
||||
}
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct Identifiers {
|
||||
pub header_from: Option<String>,
|
||||
@ -1315,6 +1511,12 @@ pub struct DmarcReportTemplate<'a> {
|
||||
pub report: &'a FormattedFeedback,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "tls_report.html")]
|
||||
pub struct TlsReportTemplate<'a> {
|
||||
pub report: &'a FormattedTlsRpt,
|
||||
}
|
||||
|
||||
// Add this helper function to parse the DMARC XML and summarize it.
|
||||
pub fn parse_dmarc_report(xml: &str) -> Result<String, ServerError> {
|
||||
let feedback: Feedback = xml_from_str(xml)
|
||||
@ -1379,25 +1581,62 @@ pub fn parse_dmarc_report(xml: &str) -> Result<String, ServerError> {
|
||||
});
|
||||
|
||||
FormattedRecord {
|
||||
source_ip: rec.row.as_ref().and_then(|r| r.source_ip.clone()).unwrap_or_else(|| "".to_string()),
|
||||
count: rec.row.as_ref().and_then(|r| r.count.map(|c| c.to_string())).unwrap_or_else(|| "".to_string()),
|
||||
header_from: rec.identifiers.as_ref().and_then(|i| i.header_from.clone()).unwrap_or_else(|| "".to_string()),
|
||||
disposition: rec.row.as_ref().and_then(|r| r.policy_evaluated.as_ref()).and_then(|p| p.disposition.clone()).unwrap_or_else(|| "".to_string()),
|
||||
dkim: rec.row.as_ref().and_then(|r| r.policy_evaluated.as_ref()).and_then(|p| p.dkim.clone()).unwrap_or_else(|| "".to_string()),
|
||||
spf: rec.row.as_ref().and_then(|r| r.policy_evaluated.as_ref()).and_then(|p| p.spf.clone()).unwrap_or_else(|| "".to_string()),
|
||||
reason: rec.row.as_ref().and_then(|r| r.policy_evaluated.as_ref()).and_then(|p| p.reason.clone()).unwrap_or_else(|| Vec::new()).into_iter().map(|r| {
|
||||
let mut s = String::new();
|
||||
if let Some(reason_type) = r.reason_type {
|
||||
s.push_str(&format!("Type: {}", reason_type));
|
||||
}
|
||||
if let Some(comment) = r.comment {
|
||||
if !s.is_empty() { s.push_str(", "); }
|
||||
s.push_str(&format!("Comment: {}", comment));
|
||||
}
|
||||
s
|
||||
}).collect(),
|
||||
auth_results,
|
||||
}
|
||||
source_ip: rec
|
||||
.row
|
||||
.as_ref()
|
||||
.and_then(|r| r.source_ip.clone())
|
||||
.unwrap_or_else(|| "".to_string()),
|
||||
count: rec
|
||||
.row
|
||||
.as_ref()
|
||||
.and_then(|r| r.count.map(|c| c.to_string()))
|
||||
.unwrap_or_else(|| "".to_string()),
|
||||
header_from: rec
|
||||
.identifiers
|
||||
.as_ref()
|
||||
.and_then(|i| i.header_from.clone())
|
||||
.unwrap_or_else(|| "".to_string()),
|
||||
disposition: rec
|
||||
.row
|
||||
.as_ref()
|
||||
.and_then(|r| r.policy_evaluated.as_ref())
|
||||
.and_then(|p| p.disposition.clone())
|
||||
.unwrap_or_else(|| "".to_string()),
|
||||
dkim: rec
|
||||
.row
|
||||
.as_ref()
|
||||
.and_then(|r| r.policy_evaluated.as_ref())
|
||||
.and_then(|p| p.dkim.clone())
|
||||
.unwrap_or_else(|| "".to_string()),
|
||||
spf: rec
|
||||
.row
|
||||
.as_ref()
|
||||
.and_then(|r| r.policy_evaluated.as_ref())
|
||||
.and_then(|p| p.spf.clone())
|
||||
.unwrap_or_else(|| "".to_string()),
|
||||
reason: rec
|
||||
.row
|
||||
.as_ref()
|
||||
.and_then(|r| r.policy_evaluated.as_ref())
|
||||
.and_then(|p| p.reason.clone())
|
||||
.unwrap_or_else(|| Vec::new())
|
||||
.into_iter()
|
||||
.map(|r| {
|
||||
let mut s = String::new();
|
||||
if let Some(reason_type) = r.reason_type {
|
||||
s.push_str(&format!("Type: {}", reason_type));
|
||||
}
|
||||
if let Some(comment) = r.comment {
|
||||
if !s.is_empty() {
|
||||
s.push_str(", ");
|
||||
}
|
||||
s.push_str(&format!("Comment: {}", comment));
|
||||
}
|
||||
s
|
||||
})
|
||||
.collect(),
|
||||
auth_results,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
|
||||
43
server/templates/tls_report.html
Normal file
43
server/templates/tls_report.html
Normal file
@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>TLS Report</title>
|
||||
</head>
|
||||
<body>
|
||||
<h3>TLS Report Summary:</h3>
|
||||
<p>Organization: {{ report.organization_name }}</p>
|
||||
<p>Date Range: {{ report.date_range.start_datetime }} to {{ report.date_range.end_datetime }}</p>
|
||||
<p>Contact: {{ report.contact_info }}</p>
|
||||
<p>Report ID: {{ report.report_id }}</p>
|
||||
|
||||
<h4>Policies:</h4>
|
||||
{% for policy in report.policies %}
|
||||
<h5>Policy Domain: {{ policy.policy.policy_domain }}</h5>
|
||||
<ul>
|
||||
<li>Policy Type: {{ policy.policy.policy_type }}</li>
|
||||
<li>Policy String: {{ policy.policy.policy_string | join(", ") }}</li>
|
||||
<li>Successful Sessions: {{ policy.summary.total_successful_session_count }}</li>
|
||||
<li>Failed Sessions: {{ policy.summary.total_failure_session_count }}</li>
|
||||
</ul>
|
||||
|
||||
<ul>
|
||||
{% for mx_host in policy.policy.mx_host %}
|
||||
<li>Hostname: {{ mx_host.hostname }}, Failures: {{ mx_host.failure_count }}, Result: {{ mx_host.result_type }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<ul>
|
||||
{% for detail in policy.failure_details %}
|
||||
<li>Result: {{ detail.result_type }}, Sending IP: {{ detail.sending_mta_ip }}, Failed Sessions: {{ detail.failed_session_count }}
|
||||
{% if detail.failure_reason_code != "" %}
|
||||
(Reason: {{ detail.failure_reason_code }})
|
||||
{% endif %}
|
||||
</li>
|
||||
(Receiving IP: {{ detail.receiving_ip }})
|
||||
(Receiving MX: {{ detail.receiving_mx_hostname }})
|
||||
(Additional Info: {{ detail.additional_info }})
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user