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::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
fs::File,
|
fs::File,
|
||||||
io::Cursor,
|
io::{Cursor, Read},
|
||||||
};
|
};
|
||||||
|
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
@ -33,6 +33,7 @@ const MESSAGE_RFC822: &'static str = "message/rfc822";
|
|||||||
const MULTIPART_ALTERNATIVE: &'static str = "multipart/alternative";
|
const MULTIPART_ALTERNATIVE: &'static str = "multipart/alternative";
|
||||||
const MULTIPART_MIXED: &'static str = "multipart/mixed";
|
const MULTIPART_MIXED: &'static str = "multipart/mixed";
|
||||||
const MULTIPART_RELATED: &'static str = "multipart/related";
|
const MULTIPART_RELATED: &'static str = "multipart/related";
|
||||||
|
const MULTIPART_REPORT: &'static str = "multipart/report";
|
||||||
const TEXT_HTML: &'static str = "text/html";
|
const TEXT_HTML: &'static str = "text/html";
|
||||||
const TEXT_PLAIN: &'static str = "text/plain";
|
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_MIXED => extract_mixed(m, part_addr),
|
||||||
MULTIPART_ALTERNATIVE => extract_alternative(m, part_addr),
|
MULTIPART_ALTERNATIVE => extract_alternative(m, part_addr),
|
||||||
MULTIPART_RELATED => extract_related(m, part_addr),
|
MULTIPART_RELATED => extract_related(m, part_addr),
|
||||||
|
MULTIPART_REPORT => extract_report(m, part_addr),
|
||||||
APPLICATION_ZIP => extract_zip(m),
|
APPLICATION_ZIP => extract_zip(m),
|
||||||
_ => extract_unhandled(m),
|
_ => extract_unhandled(m),
|
||||||
};
|
};
|
||||||
@ -528,6 +530,83 @@ fn extract_gzip(m: &ParsedMail) -> Result<Body, ServerError> {
|
|||||||
extract_unhandled(m)
|
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> {
|
fn extract_unhandled(m: &ParsedMail) -> Result<Body, ServerError> {
|
||||||
let msg = format!(
|
let msg = format!(
|
||||||
"Unhandled body content type:\n{}\n{}",
|
"Unhandled body content type:\n{}\n{}",
|
||||||
@ -1287,6 +1366,123 @@ pub struct Reason {
|
|||||||
pub reason_type: Option<String>,
|
pub reason_type: Option<String>,
|
||||||
pub comment: 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)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
pub struct Identifiers {
|
pub struct Identifiers {
|
||||||
pub header_from: Option<String>,
|
pub header_from: Option<String>,
|
||||||
@ -1315,6 +1511,12 @@ pub struct DmarcReportTemplate<'a> {
|
|||||||
pub report: &'a FormattedFeedback,
|
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.
|
// Add this helper function to parse the DMARC XML and summarize it.
|
||||||
pub fn parse_dmarc_report(xml: &str) -> Result<String, ServerError> {
|
pub fn parse_dmarc_report(xml: &str) -> Result<String, ServerError> {
|
||||||
let feedback: Feedback = xml_from_str(xml)
|
let feedback: Feedback = xml_from_str(xml)
|
||||||
@ -1379,25 +1581,62 @@ pub fn parse_dmarc_report(xml: &str) -> Result<String, ServerError> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
FormattedRecord {
|
FormattedRecord {
|
||||||
source_ip: rec.row.as_ref().and_then(|r| r.source_ip.clone()).unwrap_or_else(|| "".to_string()),
|
source_ip: rec
|
||||||
count: rec.row.as_ref().and_then(|r| r.count.map(|c| c.to_string())).unwrap_or_else(|| "".to_string()),
|
.row
|
||||||
header_from: rec.identifiers.as_ref().and_then(|i| i.header_from.clone()).unwrap_or_else(|| "".to_string()),
|
.as_ref()
|
||||||
disposition: rec.row.as_ref().and_then(|r| r.policy_evaluated.as_ref()).and_then(|p| p.disposition.clone()).unwrap_or_else(|| "".to_string()),
|
.and_then(|r| r.source_ip.clone())
|
||||||
dkim: rec.row.as_ref().and_then(|r| r.policy_evaluated.as_ref()).and_then(|p| p.dkim.clone()).unwrap_or_else(|| "".to_string()),
|
.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()),
|
count: rec
|
||||||
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| {
|
.row
|
||||||
let mut s = String::new();
|
.as_ref()
|
||||||
if let Some(reason_type) = r.reason_type {
|
.and_then(|r| r.count.map(|c| c.to_string()))
|
||||||
s.push_str(&format!("Type: {}", reason_type));
|
.unwrap_or_else(|| "".to_string()),
|
||||||
}
|
header_from: rec
|
||||||
if let Some(comment) = r.comment {
|
.identifiers
|
||||||
if !s.is_empty() { s.push_str(", "); }
|
.as_ref()
|
||||||
s.push_str(&format!("Comment: {}", comment));
|
.and_then(|i| i.header_from.clone())
|
||||||
}
|
.unwrap_or_else(|| "".to_string()),
|
||||||
s
|
disposition: rec
|
||||||
}).collect(),
|
.row
|
||||||
auth_results,
|
.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()
|
.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