diff options
Diffstat (limited to 'modules/private/websites/tools/tools/dmarc_reports')
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 | |||
3 | require(getenv("SECRETS_FILE")); | ||
4 | |||
5 | $response = array( | ||
6 | "status" => "ok", | ||
7 | ); | ||
8 | $mysqli = new mysqli($dbhost, $dbuser, $dbpass, $dbname, $dbport); | ||
9 | |||
10 | function 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']; | ||
22 | function 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 | |||
36 | if (!$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 | |||
43 | if ($mysqli->connect_errno) { | ||
44 | error_die($mysqli->connect_error, $mysqli->connect_errno); | ||
45 | } | ||
46 | |||
47 | if (!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 | |||
119 | header("Content-Type: application/json"); | ||
120 | |||
121 | echo 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 @@ | |||
1 | const 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 @@ | |||
1 | h1 { | ||
2 | text-align: center; | ||
3 | } | ||
4 | |||
5 | p.warninginfo { | ||
6 | text-align: center; | ||
7 | } | ||
8 | |||
9 | table.reportlist { | ||
10 | margin: 2em auto 2em auto; | ||
11 | border-collapse: collapse; | ||
12 | clear: both; | ||
13 | } | ||
14 | |||
15 | table.reportlist td, table.reportlist th { | ||
16 | padding:3px; | ||
17 | } | ||
18 | |||
19 | table.reportlist thead { | ||
20 | border-top: 1px solid grey; | ||
21 | border-bottom: 1px solid grey; | ||
22 | |||
23 | } | ||
24 | table.reportlist tbody tr:first-child td { | ||
25 | padding-top: 10px; | ||
26 | } | ||
27 | table.reportlist tr.sum { | ||
28 | border-top: 1px solid grey; | ||
29 | } | ||
30 | table.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 | |||
40 | tr.summaryrow { | ||
41 | cursor: pointer; | ||
42 | } | ||
43 | |||
44 | tr.summaryrow:hover, tr.summaryrow.selected { | ||
45 | background-color: lightgray; | ||
46 | border-left: 1px solid lightgray; | ||
47 | } | ||
48 | |||
49 | td.reportcell { | ||
50 | border-bottom: 1px solid lightgray; | ||
51 | border-left: 1px solid lightgray; | ||
52 | border-right: 1px solid lightgray; | ||
53 | } | ||
54 | |||
55 | table.reportdata { | ||
56 | margin: 0px auto 0px auto; | ||
57 | border-collapse: separate; | ||
58 | border-spacing: 2px; | ||
59 | } | ||
60 | |||
61 | table.reportdata tr th, table.reportdata tr td { | ||
62 | text-align: center; | ||
63 | padding: 3px; | ||
64 | } | ||
65 | |||
66 | table.reportdata tr.red { | ||
67 | background-color: #FF0000; | ||
68 | } | ||
69 | |||
70 | table.reportdata tr.orange { | ||
71 | background-color: #FFA500; | ||
72 | } | ||
73 | |||
74 | table.reportdata tr.lime { | ||
75 | background-color: #00FF00; | ||
76 | } | ||
77 | |||
78 | table.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> | ||