538 lines
16 KiB
JavaScript
538 lines
16 KiB
JavaScript
/**
|
|
* dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports.
|
|
* Copyright (C) 2020 Aleksey Andreev (liuch)
|
|
*
|
|
* Available at:
|
|
* https://github.com/liuch/dmarc-srg
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License as published by the Free
|
|
* Software Foundation, either version 3 of the License.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
* more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with
|
|
* this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
class Summary {
|
|
constructor(id) {
|
|
this._report = null;
|
|
this._element = document.getElementById("main-block");
|
|
this._container = null;
|
|
this._options_data = null;
|
|
this._options_block = null;
|
|
this._report_block = null;
|
|
}
|
|
|
|
display() {
|
|
this._create_container();
|
|
this._element.appendChild(this._container);
|
|
this._create_options_block();
|
|
this._create_report_block();
|
|
this._container.appendChild(this._options_block);
|
|
this._container.appendChild(document.createElement("hr"));
|
|
this._container.appendChild(this._report_block);
|
|
}
|
|
|
|
update() {
|
|
this._handle_url_params();
|
|
this._update_options_block();
|
|
this._fetch_report();
|
|
}
|
|
|
|
title() {
|
|
return "Summary Reports";
|
|
}
|
|
|
|
_handle_url_params() {
|
|
let url_params = new URL(document.location.href).searchParams;
|
|
let domain = url_params.get("domain");
|
|
let period = url_params.get("period");
|
|
let format = url_params.get("format");
|
|
if (domain && period) {
|
|
this._options_data = { domain: domain, period: period, format: format || "text" };
|
|
} else {
|
|
this._options_data = null;
|
|
}
|
|
}
|
|
|
|
_create_container() {
|
|
this._container = document.createElement("div");
|
|
this._container.setAttribute("class", "panel-container round-border");
|
|
}
|
|
|
|
_create_options_block() {
|
|
let opts = document.createElement("div");
|
|
opts.setAttribute("class", "options-block");
|
|
opts.appendChild(document.createTextNode("Report options: "));
|
|
opts.appendChild(document.createTextNode("none"));
|
|
|
|
let btn = document.createElement("button");
|
|
btn.setAttribute("class", "options-button");
|
|
btn.appendChild(document.createTextNode("Change"));
|
|
btn.addEventListener("click", function(event) {
|
|
this._display_dialog();
|
|
}.bind(this));
|
|
opts.appendChild(btn);
|
|
|
|
this._options_block = opts;
|
|
}
|
|
|
|
_update_options_block() {
|
|
let text = "none";
|
|
if (this._options_data) {
|
|
text = "domain=" + this._options_data.domain + " period=" + this._options_data.period;
|
|
}
|
|
this._options_block.childNodes[1].textContent = text;
|
|
}
|
|
|
|
_create_report_block() {
|
|
this._report_block = document.createElement("div");
|
|
this._report_block.setAttribute("class", "summary-report");
|
|
}
|
|
|
|
_display_dialog() {
|
|
let dlg = new OptionsDialog(this._options_data);
|
|
document.getElementById("main-block").appendChild(dlg.element());
|
|
dlg.show().then(function(d) {
|
|
if (!d) {
|
|
return;
|
|
}
|
|
let url = new URL(document.location.href);
|
|
url.searchParams.set("domain", d.domain);
|
|
let period = d.period;
|
|
if (period === "lastndays") {
|
|
period += ":" + d.days;
|
|
}
|
|
url.searchParams.set("period", period);
|
|
url.searchParams.set("format", d.format);
|
|
window.history.replaceState(null, "", url.toString());
|
|
remove_all_children(this._element);
|
|
this.display();
|
|
this.update();
|
|
}.bind(this)).finally(function() {
|
|
this._options_block.lastChild.focus();
|
|
}.bind(this));
|
|
}
|
|
|
|
_fetch_report() {
|
|
remove_all_children(this._report_block);
|
|
if (!this._options_data) {
|
|
this._report_block.appendChild(document.createTextNode("Report options are not selected"));
|
|
return;
|
|
}
|
|
this._report_block.appendChild(set_wait_status());
|
|
let uparams = new URLSearchParams();
|
|
let domain = this._options_data.domain;
|
|
uparams.set("domain", domain);
|
|
uparams.set("period", this._options_data.period);
|
|
uparams.set("format", this._options_data.format === "html" ? "raw" : "text");
|
|
window.fetch("summary.php?mode=report&" + uparams.toString(), {
|
|
method: "GET",
|
|
cache: "no-store",
|
|
headers: HTTP_HEADERS,
|
|
credentials: "same-origin"
|
|
}).then(function(resp) {
|
|
if (!resp.ok) {
|
|
throw new Error("Failed to fetch the report");
|
|
}
|
|
return resp.json();
|
|
}).then(function(report) {
|
|
Common.checkResult(report);
|
|
report.domain = domain;
|
|
this._report = new SummaryReport(report);
|
|
this._display_report();
|
|
}.bind(this)).catch(function(err) {
|
|
Common.displayError(err);
|
|
set_error_status(this._report_block, 'Error: ' + err.message);
|
|
}.bind(this)).finally(function() {
|
|
let wm = this._report_block.querySelector(".wait-message");
|
|
if (wm) {
|
|
wm.remove();
|
|
}
|
|
}.bind(this));
|
|
}
|
|
|
|
_display_report() {
|
|
let el = null;
|
|
let text = this._report.text();
|
|
if (text) {
|
|
el = document.createElement("pre");
|
|
el.appendChild(document.createTextNode(this._report.text()));
|
|
} else {
|
|
el = this._report.html();
|
|
if (!el) {
|
|
el = document.createElement("p");
|
|
el.appendChild(document.createTextNode("No data"));
|
|
}
|
|
}
|
|
this._report_block.appendChild(el);
|
|
}
|
|
}
|
|
|
|
class OptionsDialog extends ModalDialog {
|
|
constructor(params) {
|
|
super({ title: "Report options", buttons: [ "apply", "reset" ] });
|
|
this._data = params || {};
|
|
this._content = null;
|
|
this._domains = null;
|
|
this._ui_data = [
|
|
{ name: "domain", title: "Domain" },
|
|
{ name: "period", title: "Period" },
|
|
{ name: "days", title: "Days", type: "input" },
|
|
{ name: "format", title: "Format" }
|
|
];
|
|
}
|
|
|
|
_gen_content() {
|
|
let container = document.createElement("div");
|
|
container.setAttribute("class", "titled-input");
|
|
this._content.appendChild(container);
|
|
this._content.classList.add("vertical-content");
|
|
this._ui_data.forEach(function(row) {
|
|
let i_el = this._add_option_row(row.name, row.title, container, row.type);
|
|
if (row.name === "days") {
|
|
i_el.setAttribute("type", "number");
|
|
i_el.setAttribute("min", "1");
|
|
i_el.setAttribute("max", "9999");
|
|
i_el.setAttribute("value", "");
|
|
}
|
|
row.element = i_el;
|
|
}, this);
|
|
this._ui_data[1].element.addEventListener("change", function(event) {
|
|
let days_el = this._ui_data[2].element;
|
|
if (event.target.value === "lastndays") {
|
|
days_el.disabled = false;
|
|
delete days_el.dataset.disabled;
|
|
days_el.value = days_el.dataset.value || "1";
|
|
} else {
|
|
days_el.disabled = true;
|
|
days_el.dataset.value = days_el.value || "1";
|
|
days_el.dataset.disabled = true;
|
|
days_el.value = "";
|
|
}
|
|
}.bind(this));
|
|
this._update_period_element();
|
|
this._update_format_element();
|
|
|
|
if (!this._domains) {
|
|
this._fetch_data();
|
|
}
|
|
}
|
|
|
|
_submit() {
|
|
let res = {
|
|
domain: this._ui_data[0].element.value,
|
|
period: this._ui_data[1].element.value,
|
|
format: this._ui_data[3].element.value
|
|
};
|
|
if (res.period === "lastndays") {
|
|
res.days = parseInt(this._ui_data[2].element.value) || 1;
|
|
}
|
|
this._result = res;
|
|
this.hide();
|
|
}
|
|
|
|
_add_option_row(name, title, p_el, type) {
|
|
let l_el = document.createElement("label");
|
|
p_el.appendChild(l_el);
|
|
|
|
let t_el = document.createElement("span");
|
|
t_el.appendChild(document.createTextNode(title + ": "));
|
|
l_el.appendChild(t_el);
|
|
|
|
let n_el = document.createElement(type || "select");
|
|
n_el.setAttribute("name", name);
|
|
l_el.appendChild(n_el);
|
|
|
|
return n_el;
|
|
}
|
|
|
|
_update_domain_element() {
|
|
let el = this._ui_data[0].element;
|
|
remove_all_children(el);
|
|
let c_val = this._data.domain || "";
|
|
if (this._domains) {
|
|
this._domains.forEach(function(name) {
|
|
let opt = document.createElement("option");
|
|
opt.setAttribute("value", name);
|
|
if (name === c_val) {
|
|
opt.setAttribute("selected", "");
|
|
}
|
|
opt.appendChild(document.createTextNode(name));
|
|
el.appendChild(opt);
|
|
});
|
|
}
|
|
}
|
|
|
|
_update_period_element() {
|
|
let el = this._ui_data[1].element;
|
|
let c_val = this._data.period && this._data.period.split(":") || [ "lastweek" ];
|
|
[
|
|
[ "lastweek", "Last week"],
|
|
[ "lastmonth", "Last month" ],
|
|
[ "lastndays", "Last N days" ]
|
|
].forEach(function(it) {
|
|
let opt = document.createElement("option");
|
|
opt.setAttribute("value", it[0]);
|
|
if (it[0] === c_val[0]) {
|
|
opt.setAttribute("selected", "");
|
|
}
|
|
opt.appendChild(document.createTextNode(it[1]));
|
|
el.appendChild(opt);
|
|
});
|
|
if (c_val[1]) {
|
|
let val = parseInt(c_val[1]);
|
|
let i_el = this._ui_data[2].element;
|
|
i_el.setAttribute("value", val);
|
|
i_el.dataset.value = val;
|
|
}
|
|
el.dispatchEvent(new Event("change"));
|
|
}
|
|
|
|
_update_format_element() {
|
|
let el = this._ui_data[3].element;
|
|
let cv = this._data.format || "text";
|
|
[
|
|
[ "text", "Plain text" ],
|
|
[ "html", "HTML" ]
|
|
].forEach(function(it) {
|
|
let opt = document.createElement("option");
|
|
opt.setAttribute("value", it[0]);
|
|
if (it[0] === cv) {
|
|
opt.setAttribute("selected", "");
|
|
}
|
|
opt.appendChild(document.createTextNode(it[1]));
|
|
el.appendChild(opt);
|
|
});
|
|
}
|
|
|
|
_enable_ui(enable) {
|
|
let list = this._element.querySelector("form").elements;
|
|
for (let i = 0; i < list.length; ++i) {
|
|
let el = list[i];
|
|
el.disabled = !enable || el.dataset.disabled;
|
|
}
|
|
}
|
|
|
|
_fetch_data() {
|
|
this._enable_ui(false);
|
|
this._content.appendChild(set_wait_status());
|
|
window.fetch("summary.php?mode=options", {
|
|
method: "GET",
|
|
cache: "no-store",
|
|
headers: HTTP_HEADERS,
|
|
credentials: "same-origin"
|
|
}).then(function(resp) {
|
|
if (!resp.ok) {
|
|
throw new Error("Failed to fetch the report options list");
|
|
}
|
|
return resp.json();
|
|
}).then(function(data) {
|
|
Common.checkResult(data);
|
|
this._domains = data.domains;
|
|
this._update_domain_element();
|
|
this._enable_ui(true);
|
|
}.bind(this)).catch(function(err) {
|
|
Common.displayError(err);
|
|
this._content.appendChild(set_error_status());
|
|
}.bind(this)).finally(function() {
|
|
this._content.querySelector(".wait-message").remove();
|
|
}.bind(this));
|
|
}
|
|
|
|
_reset() {
|
|
window.setTimeout(function() {
|
|
this._ui_data[1].element.dispatchEvent(new Event("change"));
|
|
}.bind(this), 0);
|
|
}
|
|
}
|
|
|
|
class SummaryReport {
|
|
constructor(data) {
|
|
this._report = data;
|
|
}
|
|
|
|
text() {
|
|
let lines = this._report.text || [];
|
|
if (lines.length > 0) {
|
|
return lines.join("\n");
|
|
}
|
|
}
|
|
|
|
html() {
|
|
let data = this._report.data;
|
|
let html = document.createDocumentFragment();
|
|
let header = document.createElement("h2");
|
|
header.appendChild(document.createTextNode("Domain: " + this._report.domain));
|
|
html.appendChild(header);
|
|
{
|
|
let range = document.createElement("div");
|
|
let d1 = (new Date(data.date_range.begin)).toLocaleDateString();
|
|
let d2 = (new Date(data.date_range.end)).toLocaleDateString();
|
|
range.appendChild(document.createTextNode("Range: " + d1 + " - " + d2));
|
|
html.appendChild(range);
|
|
}
|
|
{
|
|
let header = document.createElement("h3");
|
|
header.appendChild(document.createTextNode("Summary"));
|
|
html.appendChild(header);
|
|
let cont = document.createElement("div");
|
|
cont.setAttribute("class", "left-titled");
|
|
html.appendChild(cont);
|
|
function add_row(title, value, cname) {
|
|
let te = document.createElement("span");
|
|
te.appendChild(document.createTextNode(title + ": "));
|
|
cont.appendChild(te);
|
|
let ve = document.createElement("span");
|
|
if (cname) {
|
|
ve.setAttribute("class", cname);
|
|
}
|
|
ve.appendChild(document.createTextNode(value));
|
|
cont.appendChild(ve);
|
|
}
|
|
let emails = data.summary.emails;
|
|
let total = emails.total;
|
|
add_row("Total", total);
|
|
let aligned = emails.dkim_spf_aligned + emails.dkim_aligned + emails.spf_aligned;
|
|
let n_aligned = total - aligned;
|
|
add_row(
|
|
"DKIM or SPF aligned",
|
|
SummaryReport.num2percent(aligned, total),
|
|
aligned && "report-result-pass" || null
|
|
);
|
|
add_row(
|
|
"Not aligned",
|
|
SummaryReport.num2percent(n_aligned, total),
|
|
n_aligned && "report-result-fail" || null
|
|
);
|
|
add_row("Organizations", data.summary.organizations);
|
|
}
|
|
if (data.sources && data.sources.length) {
|
|
let header = document.createElement("h3");
|
|
header.appendChild(document.createTextNode("Sources"));
|
|
html.appendChild(header);
|
|
let table = document.createElement("table");
|
|
table.setAttribute("class", "report-table");
|
|
html.appendChild(table);
|
|
|
|
let caption = document.createElement("caption");
|
|
caption.appendChild(document.createTextNode("Total records: " + data.sources.length));
|
|
table.appendChild(caption);
|
|
let thead = document.createElement("thead");
|
|
table.appendChild(thead);
|
|
[
|
|
[
|
|
[ "IP address", 0, 2 ], [ "Email volume", 0, 2 ], [ "SPF", 3, 0 ], [ "DKIM", 3, 0 ]
|
|
],
|
|
[
|
|
[ "pass" ], [ "fail" ], [ "rate" ], [ "pass" ], [ "fail" ], [ "rate" ]
|
|
]
|
|
].forEach(function(row) {
|
|
let tr = document.createElement("tr");
|
|
thead.appendChild(tr);
|
|
row.forEach(function(col) {
|
|
let th = document.createElement("th");
|
|
th.appendChild(document.createTextNode(col[0]));
|
|
if (col[1]) {
|
|
th.setAttribute("colspan", col[1]);
|
|
}
|
|
if (col[2]) {
|
|
th.setAttribute("rowspan", col[2]);
|
|
}
|
|
tr.appendChild(th);
|
|
});
|
|
});
|
|
let tbody = document.createElement("tbody");
|
|
table.appendChild(tbody);
|
|
data.sources.forEach(function(sou) {
|
|
let tr = document.createElement("tr");
|
|
tbody.appendChild(tr);
|
|
let va = [];
|
|
va.push([ Common.makeIpElement(sou.ip), 0 ]);
|
|
let ett = sou.emails;
|
|
let spf = sou.spf_aligned;
|
|
let dkm = sou.dkim_aligned;
|
|
va.push([ ett, 1 ]);
|
|
va.push([ spf, 3 ]);
|
|
va.push([ ett - spf, 5 ]);
|
|
va.push([ spf / ett, 8 ]);
|
|
va.push([ dkm, 3 ]);
|
|
va.push([ ett - dkm, 5 ]);
|
|
va.push([ dkm / ett, 8 ]);
|
|
va.forEach(function(it) {
|
|
let val = it[0];
|
|
let mode = it[1];
|
|
let td = document.createElement("td");
|
|
if (val && (mode & 2)) {
|
|
td.setAttribute("class", "report-result-pass");
|
|
}
|
|
if (val && (mode & 4)) {
|
|
td.setAttribute("class", "report-result-fail");
|
|
}
|
|
if (mode & 8) {
|
|
val = (val * 100).toFixed(0) + "%";
|
|
} else if (mode & 1) {
|
|
val = val.toLocaleString();
|
|
}
|
|
if (typeof(val) === "object") {
|
|
td.appendChild(val);
|
|
} else {
|
|
td.appendChild(document.createTextNode(val));
|
|
}
|
|
tr.appendChild(td);
|
|
});
|
|
});
|
|
}
|
|
if (data.organizations && data.organizations.length) {
|
|
let header = document.createElement("h3");
|
|
header.appendChild(document.createTextNode("Organizations"));
|
|
html.appendChild(header);
|
|
let table = document.createElement("table");
|
|
table.setAttribute("class", "report-table");
|
|
html.appendChild(table);
|
|
|
|
let caption = document.createElement("caption");
|
|
caption.appendChild(document.createTextNode("Total records: " + data.organizations.length));
|
|
table.appendChild(caption);
|
|
let thead = document.createElement("thead");
|
|
table.appendChild(thead);
|
|
let tr = document.createElement("tr");
|
|
thead.appendChild(tr);
|
|
[ "Name", "Emails", "Reports" ].forEach(function(org) {
|
|
let th = document.createElement("th");
|
|
th.appendChild(document.createTextNode(org));
|
|
tr.appendChild(th);
|
|
});
|
|
let tbody = document.createElement("tbody");
|
|
table.appendChild(tbody);
|
|
data.organizations.forEach(function(org) {
|
|
let tr = document.createElement("tr");
|
|
tbody.appendChild(tr);
|
|
let va = [];
|
|
va.push(org.name);
|
|
va.push(org.emails.toLocaleString());
|
|
va.push(org.reports.toLocaleString());
|
|
va.forEach(function(v) {
|
|
let td = document.createElement("td");
|
|
td.appendChild(document.createTextNode(v));
|
|
tr.appendChild(td);
|
|
});
|
|
});
|
|
}
|
|
return html;
|
|
}
|
|
|
|
static num2percent(per, cent) {
|
|
if (!per) {
|
|
return "0";
|
|
}
|
|
return "" + Math.round(per / cent * 100, per) + "% (" + per + ")";
|
|
}
|
|
}
|
|
|