aboutsummaryrefslogtreecommitdiff
path: root/modules/private/websites/tools/tools/dmarc_reports
diff options
context:
space:
mode:
authorIsmaël Bouya <ismael.bouya@normalesup.org>2020-04-26 03:04:56 +0200
committerIsmaël Bouya <ismael.bouya@normalesup.org>2020-04-26 03:04:56 +0200
commit7df5e532c1ce2ab9e8527615c08c1178990870e6 (patch)
tree3790f2afe0be38e37ba82305a1139db6c6b61c79 /modules/private/websites/tools/tools/dmarc_reports
parenta8ef1adb4a90c2524ac09a85463598e5d41d2a4a (diff)
downloadNix-7df5e532c1ce2ab9e8527615c08c1178990870e6.tar.gz
Nix-7df5e532c1ce2ab9e8527615c08c1178990870e6.tar.zst
Nix-7df5e532c1ce2ab9e8527615c08c1178990870e6.zip
Add dmarc reports
Diffstat (limited to 'modules/private/websites/tools/tools/dmarc_reports')
-rw-r--r--modules/private/websites/tools/tools/dmarc_reports/api.php87
-rw-r--r--modules/private/websites/tools/tools/dmarc_reports/app.js86
-rw-r--r--modules/private/websites/tools/tools/dmarc_reports/default.css126
-rw-r--r--modules/private/websites/tools/tools/dmarc_reports/index.html118
4 files changed, 417 insertions, 0 deletions
diff --git a/modules/private/websites/tools/tools/dmarc_reports/api.php b/modules/private/websites/tools/tools/dmarc_reports/api.php
new file mode 100644
index 0000000..9b7f0c0
--- /dev/null
+++ b/modules/private/websites/tools/tools/dmarc_reports/api.php
@@ -0,0 +1,87 @@
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
21if ($mysqli->connect_errno) {
22 error_die($mysqli->connect_error, $mysqli->connect_errno);
23}
24
25if (!isset($_GET['serial'])) {
26 $response["domains"] = array();
27 $query = $mysqli->query("SELECT DISTINCT domain FROM `report` ORDER BY domain");
28 if ($mysqli->error) { error_die($mysqli->error, $mysqli->errno); }
29 while($row = $query->fetch_assoc()) {
30 $response["domains"][] = $row['domain'];
31 }
32
33 $response["orgs"] = array();
34 $query = $mysqli->query("SELECT DISTINCT org FROM `report` ORDER BY org");
35 if ($mysqli->error) { error_die($mysqli->error, $mysqli->errno); }
36 while($row = $query->fetch_assoc()) {
37 $response["orgs"][] = $row['org'];
38 }
39
40 $response["dates"] = array();
41 $query = $mysqli->query("SELECT DISTINCT DISTINCT year(mindate) as year, month(mindate) as month FROM `report` ORDER BY year DESC,month DESC");
42 if ($mysqli->error) { error_die($mysqli->error, $mysqli->errno); }
43 while($row = $query->fetch_assoc()) {
44 $response["dates"][] = sprintf( "%'.04d-%'.02d", $row['year'], $row['month'] );
45 }
46
47 $response["summaries"] = array();
48 if (isset($_GET['errors_only'])) {
49 $where = " WHERE (spfresult != 'pass' or dkimresult != 'pass')";
50 } else {
51 $where = "";
52 }
53
54 $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";
55 $query = $mysqli->query($sql);
56 if ($mysqli->error) { error_die($mysqli->error, $mysqli->errno); }
57 while($row = $query->fetch_assoc()) {
58 unset($row["raw_xml"]);
59 $response["summaries"][] = $row;
60 }
61} else {
62 $response["rptrecord"] = [];
63 $sql = $mysqli->prepare("SELECT * FROM rptrecord where serial = ?");
64 $sql->bind_param("s", $_GET["serial"]);
65 $sql->execute();
66 $query = $sql->get_result();
67 if ($mysqli->error) { error_die($mysqli->error, $mysqli->errno); }
68 while($row = $query->fetch_assoc()) {
69 if ($row['ip']) {
70 $ip = long2ip($row['ip']);
71 $host = gethostbyaddr($ip);
72 } elseif ( $row['ip6'] ) {
73 $ip = inet_ntop($row['ip6']);
74 $host = gethostbyaddr($ip);
75 } else {
76 $ip = "-";
77 $host = "-";
78 }
79 $row['ip'] = $ip;
80 $row['host'] = $host;
81 unset($row['ip6']);
82 $response["rptrecord"][] = $row;
83 }
84}
85
86echo json_encode($response, JSON_PRETTY_PRINT);
87?>
diff --git a/modules/private/websites/tools/tools/dmarc_reports/app.js b/modules/private/websites/tools/tools/dmarc_reports/app.js
new file mode 100644
index 0000000..7fe67d0
--- /dev/null
+++ b/modules/private/websites/tools/tools/dmarc_reports/app.js
@@ -0,0 +1,86 @@
1const app = new Vue({
2 el: '#app',
3 data: {
4 info: null,
5 summaries: [],
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 },
14 created: async function () {
15 let that = this;
16
17 try {
18 this.info = await this.getInfo();
19 this.summaries = this.info.summaries;
20 } catch (error) {}
21 },
22 methods: {
23 filtered: function () {
24 let that = this;
25 let filtered = this.summaries.filter(function (summary) {
26 return (!that.filterGreen || that.getColor(summary) !== "lime")
27 && (!that.filterDomain || summary.domain === that.filterDomain)
28 && (!that.filterOrg || summary.org === that.filterOrg)
29 && (!that.filterDate || that.inDates(summary));
30 });
31 if (this.reverse) {
32 return filtered.reverse();
33 } else {
34 return filtered;
35 }
36 },
37 toggle: async function(summary) {
38 if (this.selectedSummary && this.selectedSummary.serial === summary.serial) {
39 this.selectedSummary = null;
40 } else {
41 if (!summary.details) {
42 summary.details = await this.getDetails(summary.serial);
43 }
44 this.selectedSummary = summary;
45 }
46 },
47 inDates: function(summary) {
48 if (!this.filterDate) { return true; }
49
50 let mindate = (new Date(summary.mindate)).toISOString().substring(0, 7);
51 let maxdate = (new Date(summary.maxdate)).toISOString().substring(0, 7);
52
53 return mindate === this.filterDate || maxdate === this.filterDate;
54 },
55 printDate: function (date) {
56 return (new Date(date)).toISOString();
57 },
58 getColor: function (element) {
59 if (element.dkimresult === "fail" && element.spfresult === "fail") {
60 return "red";
61 } else if (element.dkimresult === "fail" || element.spfresult === "fail") {
62 return "orange";
63 } else if (element.dkimresult === "pass" && element.spfresult === "pass") {
64 return "lime";
65 } else {
66 return "yellow";
67 }
68 },
69 getInfo: function (event) {
70 return fetch('api.php').then(function (response) {
71 if (response.status != 200) { return; }
72 return response.text().then(function (body) {
73 return JSON.parse(body);
74 });
75 });
76 },
77 getDetails: function (serial) {
78 return fetch(`api.php?serial=${serial}`).then(function (response) {
79 if (response.status != 200) { return; }
80 return response.text().then(function (body) {
81 return JSON.parse(body);
82 });
83 });
84 }
85 }
86});
diff --git a/modules/private/websites/tools/tools/dmarc_reports/default.css b/modules/private/websites/tools/tools/dmarc_reports/default.css
new file mode 100644
index 0000000..11608cc
--- /dev/null
+++ b/modules/private/websites/tools/tools/dmarc_reports/default.css
@@ -0,0 +1,126 @@
1h1 {
2 text-align: center;
3}
4
5table.reportlist {
6 margin: 2em auto 2em auto;
7 border-collapse: collapse;
8 clear: both;
9}
10
11table.reportlist td, table.reportlist th {
12 padding:3px;
13}
14
15table.reportlist thead {
16 border-top: 1px solid grey;
17 border-bottom: 1px solid grey;
18
19}
20table.reportlist tbody tr:first-child td {
21 padding-top: 10px;
22}
23table.reportlist tr.sum {
24 border-top: 1px solid grey;
25}
26table.reportlist tr.selected {
27 background-color: lightgrey;
28}
29.reportdesc {
30 font-weight: bold;
31 width: 90%;
32 margin-left: auto;
33 margin-right: auto;
34}
35
36tr.summaryrow {
37 cursor: pointer;
38}
39
40tr.summaryrow:hover, tr.summaryrow.selected {
41 background-color: lightgray;
42 border-left: 1px solid lightgray;
43}
44
45td.reportcell {
46 border-bottom: 1px solid lightgray;
47 border-left: 1px solid lightgray;
48 border-right: 1px solid lightgray;
49}
50
51table.reportdata {
52 margin: 0px auto 0px auto;
53 border-collapse: separate;
54 border-spacing: 2px;
55}
56
57table.reportdata tr th, table.reportdata tr td {
58 text-align: center;
59 padding: 3px;
60}
61
62table.reportdata tr.red {
63 background-color: #FF0000;
64}
65
66table.reportdata tr.orange {
67 background-color: #FFA500;
68}
69
70table.reportdata tr.lime {
71 background-color: #00FF00;
72}
73
74table.reportdata tr.yellow {
75 background-color: #FFFF00;
76}
77
78.optionblock {
79 background: lightgrey;
80 padding: 0.4em;
81 float: right;
82 margin: auto 2em 1em auto;
83 white-space: nowrap;
84}
85
86.optionlabel {
87 font-weight: bold;
88 float: left; clear: left;
89 margin-right: 1em;
90}
91
92.options {
93 font-size: 70%;
94 text-align: right;
95 border: none;
96 width: 97%;
97 padding: 0.4em;
98}
99
100.center {
101 text-align:center;
102}
103
104.circle_lime:before {
105 content: ' \25CF';
106 font-size: 25px;
107 color: #00FF00;
108}
109
110.circle_red:before {
111 content: ' \25CF';
112 font-size: 25px;
113 color: #FF0000;
114}
115
116.circle_yellow:before {
117 content: ' \25CF';
118 font-size: 25px;
119 color: #FFFF00;
120}
121
122.circle_orange:before {
123 content: ' \25CF';
124 font-size: 25px;
125 color: #FFA500;
126}
diff --git a/modules/private/websites/tools/tools/dmarc_reports/index.html b/modules/private/websites/tools/tools/dmarc_reports/index.html
new file mode 100644
index 0000000..ded05e1
--- /dev/null
+++ b/modules/private/websites/tools/tools/dmarc_reports/index.html
@@ -0,0 +1,118 @@
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" v-if="info">
15 <div class='options'>
16 <span class='optionlabel'>Hide all-green lines:</span>
17 <label><input type="radio" :value="false" v-model="filterGreen"> no</label>
18 <label><input type="radio" :value="true" v-model="filterGreen"> yes</label>
19 </div>
20 <div class='options'>
21 <span class='optionlabel'>Sort order:</span>
22 <label><input type="radio" :value="false" v-model="reverse"> ascending</label>
23 <label><input type="radio" :value="true" v-model="reverse"> descending</label>
24 </div>
25 <div class='options'>
26 <span class='optionlabel'>Domain(s):</span>
27 <select v-model="filterDomain">
28 <option selected="selected" :value="null">[all]</option>
29 <option v-for="domain in info.domains" :value="domain">{{ domain }}</option>
30 </select>
31 </div>
32 <div class='options'>
33 <span class='optionlabel'>Organisation(s):</span>
34 <select v-model="filterOrg">
35 <option selected="selected" :value="null">[all]</option>
36 <option v-for="org in info.orgs" :value="org">{{ org }}</option>
37 </select>
38 </div>
39 <div class='options'>
40 <span class='optionlabel'>Time:</span>
41 <select v-model="filterDate">
42 <option selected="selected" :value="null">[all]</option>
43 <option v-for="date in info.dates" :value="date">{{ date }}</option>
44 </select>
45 </div>
46 </div>
47
48 <h1 class='main'>DMARC Reports</h1>
49 <table class='reportlist' v-if="summaries">
50 <thead>
51 <tr>
52 <th></th>
53 <th>Start Date</th>
54 <th>End Date</th>
55 <th>Domain</th>
56 <th>Reporting Organization</th>
57 <th>Report ID</th>
58 <th>Messages</th>
59 </tr>
60 </thead>
61 <tbody>
62 <template v-for="summary in filtered()">
63 <tr v-on:click="toggle(summary)" class="summaryrow"
64 v-bind:class="[{ selected: selectedSummary && summary.serial === selectedSummary.serial }]">
65 <td class='right'><span :class="'circle_' + getColor(summary)"></span></td>
66 <td class='right'>{{ printDate(summary.mindate) }}</td>
67 <td class='right'>{{ printDate(summary.maxdate) }}</td>
68 <td class='center'>{{ summary.domain }}</td>
69 <td class='center'>{{ summary.org }}</td>
70 <td class='center'>{{ summary.reportid }}</td>
71 <td class='center'>{{ summary.rcount }}</td>
72 </tr>
73 <tr v-if="selectedSummary && summary.serial === selectedSummary.serial">
74 <td colspan="6" class="reportcell">
75 <div class='center reportdesc'>
76 <p>Policies: adkim={{ summary.policy_adkim }}, aspf={{ summary.policy_aspf }}, p={{ summary.policy_none }}, sp={{ summary.policy_sp }}, pct={{ summary.policy_pct }}</p>
77 </div>
78 <table v-if="summary.details" class='reportdata'>
79 <thead>
80 <tr>
81 <th>IP Address</th>
82 <th>Host Name</th>
83 <th>Message Count</th>
84 <th>Disposition</th>
85 <th>Reason</th>
86 <th>DKIM Domain</th>
87 <th>Raw DKIM Result</th>
88 <th>SPF Domain</th>
89 <th>Raw SPF Result</th>
90 </tr>
91 </thead>
92 <tbody>
93 <tr v-for="record in summary.details.rptrecord" :class='getColor(record)'>
94 <td>{{ record.ip }}</td>
95 <td>{{ record.host }}</td>
96 <td>{{ record.rcount }}</td>
97 <td>{{ record.disposition }}</td>
98 <td>{{ record.reason }}</td>
99 <td>{{ record.dkimdomain }}</td>
100 <td>{{ record.dkimresult }}</td>
101 <td>{{ record.spfdomain }}</td>
102 <td>{{ record.spfresult }}</td>
103 </tr>
104 </tbody>
105 </table>
106 </td>
107 <td></td>
108 </tr>
109 </template>
110 </tbody>
111 </table>
112 </div>
113
114 <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
115 <script src="app.js"></script>
116</body>
117
118</html>