aboutsummaryrefslogtreecommitdiff
path: root/modules/private/websites/tools/tools/dmarc_reports
diff options
context:
space:
mode:
Diffstat (limited to 'modules/private/websites/tools/tools/dmarc_reports')
-rw-r--r--modules/private/websites/tools/tools/dmarc_reports/api.php122
-rw-r--r--modules/private/websites/tools/tools/dmarc_reports/app.js103
-rw-r--r--modules/private/websites/tools/tools/dmarc_reports/default.css130
-rw-r--r--modules/private/websites/tools/tools/dmarc_reports/index.html128
4 files changed, 0 insertions, 483 deletions
diff --git a/modules/private/websites/tools/tools/dmarc_reports/api.php b/modules/private/websites/tools/tools/dmarc_reports/api.php
deleted file mode 100644
index 850f9ce..0000000
--- a/modules/private/websites/tools/tools/dmarc_reports/api.php
+++ /dev/null
@@ -1,122 +0,0 @@
1<?php
2
3require(getenv("SECRETS_FILE"));
4
5$response = array(
6 "status" => "ok",
7);
8$mysqli = new mysqli($dbhost, $dbuser, $dbpass, $dbname, $dbport);
9
10function error_die($text, $number) {
11 http_response_code("500");
12 $message = array(
13 "status" => "error",
14 "message" => $text,
15 "code" => $number
16 );
17
18 die(json_encode($message));
19}
20
21$anonymous = isset($_GET['anonymous']) && $_GET['anonymous'];
22function maybe_anonymize($string, $long = false) {
23 global $anonymous_key;
24 global $anonymous;
25 if ($anonymous) {
26 if ($long) {
27 return md5($anonymous_key . ":" . $string);
28 } else {
29 return substr(md5($anonymous_key . ":" . $string), 0, 6);
30 }
31 } else {
32 return $string;
33 }
34}
35
36if (!$anonymous && (!isset($_SERVER['HTTP_AUTHORIZATION']) || $_SERVER['HTTP_AUTHORIZATION'] === "")) {
37 header('WWW-Authenticate: Basic realm="Immae"');
38 header('HTTP/1.0 401 Unauthorized');
39 echo "You need to be authenticated to access private information";
40 exit;
41}
42
43if ($mysqli->connect_errno) {
44 error_die($mysqli->connect_error, $mysqli->connect_errno);
45}
46
47if (!isset($_GET['serial'])) {
48 $response["domains"] = array();
49 $query = $mysqli->query("SELECT DISTINCT domain FROM `report` ORDER BY domain");
50 if ($mysqli->error) { error_die($mysqli->error, $mysqli->errno); }
51 while($row = $query->fetch_assoc()) {
52 $response["domains"][] = maybe_anonymize($row['domain']);
53 }
54
55 $response["orgs"] = array();
56 $query = $mysqli->query("SELECT DISTINCT org FROM `report` ORDER BY org");
57 if ($mysqli->error) { error_die($mysqli->error, $mysqli->errno); }
58 while($row = $query->fetch_assoc()) {
59 $response["orgs"][] = maybe_anonymize($row['org']);
60 }
61
62 $response["dates"] = array();
63 $query = $mysqli->query("SELECT DISTINCT DISTINCT year(mindate) as year, month(mindate) as month FROM `report` ORDER BY year DESC,month DESC");
64 if ($mysqli->error) { error_die($mysqli->error, $mysqli->errno); }
65 while($row = $query->fetch_assoc()) {
66 $response["dates"][] = sprintf( "%'.04d-%'.02d", $row['year'], $row['month'] );
67 }
68
69 $response["summaries"] = array();
70 if (isset($_GET['errors_only'])) {
71 $where = " WHERE (spfresult != 'pass' or dkimresult != 'pass')";
72 } else {
73 $where = "";
74 }
75
76 $sql = "SELECT report.* , sum(rptrecord.rcount) AS rcount, MIN(rptrecord.dkimresult) AS dkimresult, MIN(rptrecord.spfresult) AS spfresult FROM report LEFT JOIN (SELECT rcount, COALESCE(dkimresult, 'neutral') AS dkimresult, COALESCE(spfresult, 'neutral') AS spfresult, serial FROM rptrecord) AS rptrecord ON report.serial = rptrecord.serial$where GROUP BY serial ORDER BY mindate ASC, maxdate ASC, org";
77 $query = $mysqli->query($sql);
78 if ($mysqli->error) { error_die($mysqli->error, $mysqli->errno); }
79 while($row = $query->fetch_assoc()) {
80 $wanted_keys = array(
81 'domain', 'org', 'reportid', 'mindate', 'maxdate', 'rcount', 'serial', 'policy_adkim', 'policy_aspf', 'policy_none', 'policy_sp', 'policy_pct', 'spfresult', 'dkimresult'
82 );
83 $row = array_intersect_key($row, array_fill_keys($wanted_keys, '1'));
84 $row["domain"] = maybe_anonymize($row["domain"]);
85 $row["org"] = maybe_anonymize($row["org"]);
86 $row["reportid"] = maybe_anonymize($row["reportid"], true);
87 $response["summaries"][] = $row;
88 }
89} else {
90 $response["rptrecord"] = [];
91 $sql = $mysqli->prepare("SELECT * FROM rptrecord where serial = ?");
92 $sql->bind_param("s", $_GET["serial"]);
93 $sql->execute();
94 $query = $sql->get_result();
95 if ($mysqli->error) { error_die($mysqli->error, $mysqli->errno); }
96 while($row = $query->fetch_assoc()) {
97 if ($row['ip']) {
98 $ip = long2ip($row['ip']);
99 $host = gethostbyaddr($ip);
100 } elseif ( $row['ip6'] ) {
101 $ip = inet_ntop($row['ip6']);
102 $host = gethostbyaddr($ip);
103 } else {
104 $ip = "-";
105 $host = "-";
106 }
107 $wanted_keys = array(
108 'ip', 'host', 'rcount', 'disposition', 'reason', 'dkimdomain', 'dkimresult', 'spfdomain', 'spfresult'
109 );
110 $row = array_intersect_key($row, array_fill_keys($wanted_keys, '1'));
111 $row['ip'] = maybe_anonymize($ip);
112 $row['host'] = maybe_anonymize($host);
113 $row['dkimdomain'] = maybe_anonymize($row['dkimdomain']);
114 $row['spfdomain'] = maybe_anonymize($row['spfdomain']);
115 $response["rptrecord"][] = $row;
116 }
117}
118
119header("Content-Type: application/json");
120
121echo json_encode($response, JSON_PRETTY_PRINT);
122?>
diff --git a/modules/private/websites/tools/tools/dmarc_reports/app.js b/modules/private/websites/tools/tools/dmarc_reports/app.js
deleted file mode 100644
index 8e8a6c4..0000000
--- a/modules/private/websites/tools/tools/dmarc_reports/app.js
+++ /dev/null
@@ -1,103 +0,0 @@
1const app = new Vue({
2 el: '#app',
3 data: {
4 info: null,
5 summaries: null,
6 selectedSummary: null,
7 filterGreen: true,
8 filterDomain: null,
9 filterOrg: null,
10 //filterDate: (new Date()).toISOString().substring(0, 7),
11 filterDate: null,
12 reverse: true,
13 anonymous: true,
14 },
15 created: async function () {
16 let that = this;
17
18 if ('anonymous' in localStorage) {
19 this.anonymous = JSON.parse(localStorage.anonymous);
20 }
21 this.fetchAll();
22 },
23 methods: {
24 fetchAll: async function() {
25 try {
26 this.info = await this.getInfo();
27 this.summaries = this.info.summaries;
28 } catch (error) {
29 this.info = null;
30 this.summaries = null;
31 }
32 },
33 toggleAnonymous: function() {
34 this.anonymous = !this.anonymous;
35 localStorage.anonymous = this.anonymous;
36 this.fetchAll();
37 },
38 filtered: function () {
39 let that = this;
40 let filtered = this.summaries.filter(function (summary) {
41 return (!that.filterGreen || that.getColor(summary) !== "lime")
42 && (!that.filterDomain || summary.domain === that.filterDomain)
43 && (!that.filterOrg || summary.org === that.filterOrg)
44 && (!that.filterDate || that.inDates(summary));
45 });
46 if (this.reverse) {
47 return filtered.reverse();
48 } else {
49 return filtered;
50 }
51 },
52 toggle: async function(summary) {
53 if (this.selectedSummary && this.selectedSummary.serial === summary.serial) {
54 this.selectedSummary = null;
55 } else {
56 if (!summary.details) {
57 summary.details = await this.getDetails(summary.serial);
58 }
59 this.selectedSummary = summary;
60 }
61 },
62 inDates: function(summary) {
63 if (!this.filterDate) { return true; }
64
65 let mindate = (new Date(summary.mindate)).toISOString().substring(0, 7);
66 let maxdate = (new Date(summary.maxdate)).toISOString().substring(0, 7);
67
68 return mindate === this.filterDate || maxdate === this.filterDate;
69 },
70 printDate: function (date) {
71 return (new Date(date)).toISOString().replace("T", " ").replace(/\..*Z$/, " UTC");
72 },
73 getColor: function (element) {
74 if (element.dkimresult === "fail" && element.spfresult === "fail") {
75 return "red";
76 } else if (element.dkimresult === "fail" || element.spfresult === "fail") {
77 return "orange";
78 } else if (element.dkimresult === "pass" && element.spfresult === "pass") {
79 return "lime";
80 } else {
81 return "yellow";
82 }
83 },
84 getInfo: function (event) {
85 let anonymous = this.anonymous ? "anonymous=1" : "";
86 return fetch(`api.php?${anonymous}`).then(function (response) {
87 if (response.status != 200) { return; }
88 return response.text().then(function (body) {
89 return JSON.parse(body);
90 });
91 });
92 },
93 getDetails: function (serial) {
94 let anonymous = this.anonymous ? "&anonymous=1" : "";
95 return fetch(`api.php?serial=${serial}${anonymous}`).then(function (response) {
96 if (response.status != 200) { return; }
97 return response.text().then(function (body) {
98 return JSON.parse(body);
99 });
100 });
101 }
102 }
103});
diff --git a/modules/private/websites/tools/tools/dmarc_reports/default.css b/modules/private/websites/tools/tools/dmarc_reports/default.css
deleted file mode 100644
index 9e0c63f..0000000
--- a/modules/private/websites/tools/tools/dmarc_reports/default.css
+++ /dev/null
@@ -1,130 +0,0 @@
1h1 {
2 text-align: center;
3}
4
5p.warninginfo {
6 text-align: center;
7}
8
9table.reportlist {
10 margin: 2em auto 2em auto;
11 border-collapse: collapse;
12 clear: both;
13}
14
15table.reportlist td, table.reportlist th {
16 padding:3px;
17}
18
19table.reportlist thead {
20 border-top: 1px solid grey;
21 border-bottom: 1px solid grey;
22
23}
24table.reportlist tbody tr:first-child td {
25 padding-top: 10px;
26}
27table.reportlist tr.sum {
28 border-top: 1px solid grey;
29}
30table.reportlist tr.selected {
31 background-color: lightgrey;
32}
33.reportdesc {
34 font-weight: bold;
35 width: 90%;
36 margin-left: auto;
37 margin-right: auto;
38}
39
40tr.summaryrow {
41 cursor: pointer;
42}
43
44tr.summaryrow:hover, tr.summaryrow.selected {
45 background-color: lightgray;
46 border-left: 1px solid lightgray;
47}
48
49td.reportcell {
50 border-bottom: 1px solid lightgray;
51 border-left: 1px solid lightgray;
52 border-right: 1px solid lightgray;
53}
54
55table.reportdata {
56 margin: 0px auto 0px auto;
57 border-collapse: separate;
58 border-spacing: 2px;
59}
60
61table.reportdata tr th, table.reportdata tr td {
62 text-align: center;
63 padding: 3px;
64}
65
66table.reportdata tr.red {
67 background-color: #FF0000;
68}
69
70table.reportdata tr.orange {
71 background-color: #FFA500;
72}
73
74table.reportdata tr.lime {
75 background-color: #00FF00;
76}
77
78table.reportdata tr.yellow {
79 background-color: #FFFF00;
80}
81
82.optionblock {
83 background: lightgrey;
84 padding: 0.4em;
85 float: right;
86 margin: auto 2em 1em auto;
87 white-space: nowrap;
88}
89
90.optionlabel {
91 font-weight: bold;
92 float: left; clear: left;
93 margin-right: 1em;
94}
95
96.options {
97 font-size: 70%;
98 text-align: right;
99 border: none;
100 width: 97%;
101 padding: 0.4em;
102}
103
104.center {
105 text-align:center;
106}
107
108.circle_lime:before {
109 content: ' \25CF';
110 font-size: 25px;
111 color: #00FF00;
112}
113
114.circle_red:before {
115 content: ' \25CF';
116 font-size: 25px;
117 color: #FF0000;
118}
119
120.circle_yellow:before {
121 content: ' \25CF';
122 font-size: 25px;
123 color: #FFFF00;
124}
125
126.circle_orange:before {
127 content: ' \25CF';
128 font-size: 25px;
129 color: #FFA500;
130}
diff --git a/modules/private/websites/tools/tools/dmarc_reports/index.html b/modules/private/websites/tools/tools/dmarc_reports/index.html
deleted file mode 100644
index 0afc82f..0000000
--- a/modules/private/websites/tools/tools/dmarc_reports/index.html
+++ /dev/null
@@ -1,128 +0,0 @@
1<!DOCTYPE html>
2<html>
3
4<head>
5 <meta charset="utf-8">
6 <meta name="viewport" content="width=device-width, initial-scale=1">
7 <meta name="robots" content="noindex">
8 <title>Dmarc reports</title>
9 <link rel="stylesheet" href="default.css">
10</head>
11
12<body>
13 <div id="app" style="width: 100%">
14 <div class="optionblock">
15 <div class='options'>
16 <span class='optionlabel'>Anonymize</span>
17 <label><input type="radio" :value="false" v-model="anonymous" v-on:click="toggleAnonymous()"> no</label>
18 <label><input type="radio" :value="true" v-model="anonymous" v-on:click="toggleAnonymous()"> yes</label>
19 </div>
20 <template v-if="info">
21 <div class='options'>
22 <span class='optionlabel'>Hide all-green lines:</span>
23 <label><input type="radio" :value="false" v-model="filterGreen"> no</label>
24 <label><input type="radio" :value="true" v-model="filterGreen"> yes</label>
25 </div>
26 <div class='options'>
27 <span class='optionlabel'>Sort order:</span>
28 <label><input type="radio" :value="false" v-model="reverse"> ascending</label>
29 <label><input type="radio" :value="true" v-model="reverse"> descending</label>
30 </div>
31 <div class='options'>
32 <span class='optionlabel'>Domain(s):</span>
33 <select v-model="filterDomain">
34 <option selected="selected" :value="null">[all]</option>
35 <option v-for="domain in info.domains" :value="domain">{{ domain }}</option>
36 </select>
37 </div>
38 <div class='options'>
39 <span class='optionlabel'>Organisation(s):</span>
40 <select v-model="filterOrg">
41 <option selected="selected" :value="null">[all]</option>
42 <option v-for="org in info.orgs" :value="org">{{ org }}</option>
43 </select>
44 </div>
45 <div class='options'>
46 <span class='optionlabel'>Time:</span>
47 <select v-model="filterDate">
48 <option selected="selected" :value="null">[all]</option>
49 <option v-for="date in info.dates" :value="date">{{ date }}</option>
50 </select>
51 </div>
52 </template>
53 </div>
54
55 <h1 class='main'>DMARC Reports</h1>
56 <p v-if="!info" class="warninginfo">
57 No information could be fetched. If in non-anonymous mode you need to be logged-in
58 </p>
59 <table class='reportlist' v-if="summaries">
60 <thead>
61 <tr>
62 <th></th>
63 <th>Start Date</th>
64 <th>End Date</th>
65 <th>Domain</th>
66 <th>Reporting Organization</th>
67 <th>Report ID</th>
68 <th>Messages</th>
69 </tr>
70 </thead>
71 <tbody>
72 <template v-for="summary in filtered()">
73 <tr v-on:click="toggle(summary)" class="summaryrow"
74 v-bind:class="[{ selected: selectedSummary && summary.serial === selectedSummary.serial }]">
75 <td class='right'><span :class="'circle_' + getColor(summary)"></span></td>
76 <td class='right'>{{ printDate(summary.mindate) }}</td>
77 <td class='right'>{{ printDate(summary.maxdate) }}</td>
78 <td class='center'>{{ summary.domain }}</td>
79 <td class='center'>{{ summary.org }}</td>
80 <td class='center'>{{ summary.reportid }}</td>
81 <td class='center'>{{ summary.rcount }}</td>
82 </tr>
83 <tr v-if="selectedSummary && summary.serial === selectedSummary.serial">
84 <td colspan="6" class="reportcell">
85 <div class='center reportdesc'>
86 <p>Policies: adkim={{ summary.policy_adkim }}, aspf={{ summary.policy_aspf }}, p={{ summary.policy_none }}, sp={{ summary.policy_sp }}, pct={{ summary.policy_pct }}</p>
87 </div>
88 <table v-if="summary.details" class='reportdata'>
89 <thead>
90 <tr>
91 <th>IP Address</th>
92 <th>Host Name</th>
93 <th>Message Count</th>
94 <th>Disposition</th>
95 <th>Reason</th>
96 <th>DKIM Domain</th>
97 <th>Raw DKIM Result</th>
98 <th>SPF Domain</th>
99 <th>Raw SPF Result</th>
100 </tr>
101 </thead>
102 <tbody>
103 <tr v-for="record in summary.details.rptrecord" :class='getColor(record)'>
104 <td>{{ record.ip }}</td>
105 <td>{{ record.host }}</td>
106 <td>{{ record.rcount }}</td>
107 <td>{{ record.disposition }}</td>
108 <td>{{ record.reason }}</td>
109 <td>{{ record.dkimdomain }}</td>
110 <td>{{ record.dkimresult }}</td>
111 <td>{{ record.spfdomain }}</td>
112 <td>{{ record.spfresult }}</td>
113 </tr>
114 </tbody>
115 </table>
116 </td>
117 <td></td>
118 </tr>
119 </template>
120 </tbody>
121 </table>
122 </div>
123
124 <script src="https://assets.immae.eu/vue/2.6.11/vue.min.js" integrity="sha256-ngFW3UnAN0Tnm76mDuu7uUtYEcG3G5H1+zioJw3t+68=" crossorigin="anonymous"></script>
125 <script src="app.js"></script>
126</body>
127
128</html>