diff options
6 files changed, 211 insertions, 141 deletions
diff --git a/modules/private/environment.nix b/modules/private/environment.nix index 5fbd023..0002622 100644 --- a/modules/private/environment.nix +++ b/modules/private/environment.nix | |||
@@ -825,6 +825,7 @@ in | |||
825 | type = submodule { | 825 | type = submodule { |
826 | options = { | 826 | options = { |
827 | mysql = mkMysqlOptions "DMARC" {}; | 827 | mysql = mkMysqlOptions "DMARC" {}; |
828 | anonymous_key = mkOption { type = str; description = "Anonymous hashing key"; }; | ||
828 | }; | 829 | }; |
829 | }; | 830 | }; |
830 | }; | 831 | }; |
diff --git a/modules/private/websites/tools/tools/dmarc_reports.nix b/modules/private/websites/tools/tools/dmarc_reports.nix index 2e44526..e264e80 100644 --- a/modules/private/websites/tools/tools/dmarc_reports.nix +++ b/modules/private/websites/tools/tools/dmarc_reports.nix | |||
@@ -12,6 +12,7 @@ rec { | |||
12 | $dbuser = "${env.mysql.user}"; | 12 | $dbuser = "${env.mysql.user}"; |
13 | $dbpass = "${env.mysql.password}"; | 13 | $dbpass = "${env.mysql.password}"; |
14 | $dbport = "${env.mysql.port}"; | 14 | $dbport = "${env.mysql.port}"; |
15 | $anonymous_key = "${env.anonymous_key}"; | ||
15 | ?> | 16 | ?> |
16 | ''; | 17 | ''; |
17 | }]; | 18 | }]; |
@@ -32,7 +33,11 @@ rec { | |||
32 | 33 | ||
33 | AllowOverride None | 34 | AllowOverride None |
34 | Options +FollowSymlinks | 35 | Options +FollowSymlinks |
36 | |||
37 | SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 | ||
38 | Use LDAPConnect | ||
35 | Require all granted | 39 | Require all granted |
40 | Require ldap-attribute uid=immae | ||
36 | </Directory> | 41 | </Directory> |
37 | ''; | 42 | ''; |
38 | }; | 43 | }; |
diff --git a/modules/private/websites/tools/tools/dmarc_reports/api.php b/modules/private/websites/tools/tools/dmarc_reports/api.php index 9b7f0c0..5d4657e 100644 --- a/modules/private/websites/tools/tools/dmarc_reports/api.php +++ b/modules/private/websites/tools/tools/dmarc_reports/api.php | |||
@@ -18,6 +18,28 @@ function error_die($text, $number) { | |||
18 | die(json_encode($message)); | 18 | die(json_encode($message)); |
19 | } | 19 | } |
20 | 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 | |||
21 | if ($mysqli->connect_errno) { | 43 | if ($mysqli->connect_errno) { |
22 | error_die($mysqli->connect_error, $mysqli->connect_errno); | 44 | error_die($mysqli->connect_error, $mysqli->connect_errno); |
23 | } | 45 | } |
@@ -27,14 +49,14 @@ if (!isset($_GET['serial'])) { | |||
27 | $query = $mysqli->query("SELECT DISTINCT domain FROM `report` ORDER BY domain"); | 49 | $query = $mysqli->query("SELECT DISTINCT domain FROM `report` ORDER BY domain"); |
28 | if ($mysqli->error) { error_die($mysqli->error, $mysqli->errno); } | 50 | if ($mysqli->error) { error_die($mysqli->error, $mysqli->errno); } |
29 | while($row = $query->fetch_assoc()) { | 51 | while($row = $query->fetch_assoc()) { |
30 | $response["domains"][] = $row['domain']; | 52 | $response["domains"][] = maybe_anonymize($row['domain']); |
31 | } | 53 | } |
32 | 54 | ||
33 | $response["orgs"] = array(); | 55 | $response["orgs"] = array(); |
34 | $query = $mysqli->query("SELECT DISTINCT org FROM `report` ORDER BY org"); | 56 | $query = $mysqli->query("SELECT DISTINCT org FROM `report` ORDER BY org"); |
35 | if ($mysqli->error) { error_die($mysqli->error, $mysqli->errno); } | 57 | if ($mysqli->error) { error_die($mysqli->error, $mysqli->errno); } |
36 | while($row = $query->fetch_assoc()) { | 58 | while($row = $query->fetch_assoc()) { |
37 | $response["orgs"][] = $row['org']; | 59 | $response["orgs"][] = maybe_anonymize($row['org']); |
38 | } | 60 | } |
39 | 61 | ||
40 | $response["dates"] = array(); | 62 | $response["dates"] = array(); |
@@ -55,7 +77,13 @@ if (!isset($_GET['serial'])) { | |||
55 | $query = $mysqli->query($sql); | 77 | $query = $mysqli->query($sql); |
56 | if ($mysqli->error) { error_die($mysqli->error, $mysqli->errno); } | 78 | if ($mysqli->error) { error_die($mysqli->error, $mysqli->errno); } |
57 | while($row = $query->fetch_assoc()) { | 79 | while($row = $query->fetch_assoc()) { |
58 | unset($row["raw_xml"]); | 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); | ||
59 | $response["summaries"][] = $row; | 87 | $response["summaries"][] = $row; |
60 | } | 88 | } |
61 | } else { | 89 | } else { |
@@ -76,9 +104,14 @@ if (!isset($_GET['serial'])) { | |||
76 | $ip = "-"; | 104 | $ip = "-"; |
77 | $host = "-"; | 105 | $host = "-"; |
78 | } | 106 | } |
79 | $row['ip'] = $ip; | 107 | $wanted_keys = array( |
80 | $row['host'] = $host; | 108 | 'ip', 'host', 'rcount', 'disposition', 'reason', 'dkimdomain', 'dkimresult', 'spfdomain', 'spfresult' |
81 | unset($row['ip6']); | 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']); | ||
82 | $response["rptrecord"][] = $row; | 115 | $response["rptrecord"][] = $row; |
83 | } | 116 | } |
84 | } | 117 | } |
diff --git a/modules/private/websites/tools/tools/dmarc_reports/app.js b/modules/private/websites/tools/tools/dmarc_reports/app.js index 7fe67d0..8e8a6c4 100644 --- a/modules/private/websites/tools/tools/dmarc_reports/app.js +++ b/modules/private/websites/tools/tools/dmarc_reports/app.js | |||
@@ -2,7 +2,7 @@ const app = new Vue({ | |||
2 | el: '#app', | 2 | el: '#app', |
3 | data: { | 3 | data: { |
4 | info: null, | 4 | info: null, |
5 | summaries: [], | 5 | summaries: null, |
6 | selectedSummary: null, | 6 | selectedSummary: null, |
7 | filterGreen: true, | 7 | filterGreen: true, |
8 | filterDomain: null, | 8 | filterDomain: null, |
@@ -10,16 +10,31 @@ const app = new Vue({ | |||
10 | //filterDate: (new Date()).toISOString().substring(0, 7), | 10 | //filterDate: (new Date()).toISOString().substring(0, 7), |
11 | filterDate: null, | 11 | filterDate: null, |
12 | reverse: true, | 12 | reverse: true, |
13 | anonymous: true, | ||
13 | }, | 14 | }, |
14 | created: async function () { | 15 | created: async function () { |
15 | let that = this; | 16 | let that = this; |
16 | 17 | ||
17 | try { | 18 | if ('anonymous' in localStorage) { |
18 | this.info = await this.getInfo(); | 19 | this.anonymous = JSON.parse(localStorage.anonymous); |
19 | this.summaries = this.info.summaries; | 20 | } |
20 | } catch (error) {} | 21 | this.fetchAll(); |
21 | }, | 22 | }, |
22 | methods: { | 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 | }, | ||
23 | filtered: function () { | 38 | filtered: function () { |
24 | let that = this; | 39 | let that = this; |
25 | let filtered = this.summaries.filter(function (summary) { | 40 | let filtered = this.summaries.filter(function (summary) { |
@@ -53,7 +68,7 @@ const app = new Vue({ | |||
53 | return mindate === this.filterDate || maxdate === this.filterDate; | 68 | return mindate === this.filterDate || maxdate === this.filterDate; |
54 | }, | 69 | }, |
55 | printDate: function (date) { | 70 | printDate: function (date) { |
56 | return (new Date(date)).toISOString(); | 71 | return (new Date(date)).toISOString().replace("T", " ").replace(/\..*Z$/, " UTC"); |
57 | }, | 72 | }, |
58 | getColor: function (element) { | 73 | getColor: function (element) { |
59 | if (element.dkimresult === "fail" && element.spfresult === "fail") { | 74 | if (element.dkimresult === "fail" && element.spfresult === "fail") { |
@@ -67,7 +82,8 @@ const app = new Vue({ | |||
67 | } | 82 | } |
68 | }, | 83 | }, |
69 | getInfo: function (event) { | 84 | getInfo: function (event) { |
70 | return fetch('api.php').then(function (response) { | 85 | let anonymous = this.anonymous ? "anonymous=1" : ""; |
86 | return fetch(`api.php?${anonymous}`).then(function (response) { | ||
71 | if (response.status != 200) { return; } | 87 | if (response.status != 200) { return; } |
72 | return response.text().then(function (body) { | 88 | return response.text().then(function (body) { |
73 | return JSON.parse(body); | 89 | return JSON.parse(body); |
@@ -75,7 +91,8 @@ const app = new Vue({ | |||
75 | }); | 91 | }); |
76 | }, | 92 | }, |
77 | getDetails: function (serial) { | 93 | getDetails: function (serial) { |
78 | return fetch(`api.php?serial=${serial}`).then(function (response) { | 94 | let anonymous = this.anonymous ? "&anonymous=1" : ""; |
95 | return fetch(`api.php?serial=${serial}${anonymous}`).then(function (response) { | ||
79 | if (response.status != 200) { return; } | 96 | if (response.status != 200) { return; } |
80 | return response.text().then(function (body) { | 97 | return response.text().then(function (body) { |
81 | return JSON.parse(body); | 98 | return JSON.parse(body); |
diff --git a/modules/private/websites/tools/tools/dmarc_reports/default.css b/modules/private/websites/tools/tools/dmarc_reports/default.css index 11608cc..9e0c63f 100644 --- a/modules/private/websites/tools/tools/dmarc_reports/default.css +++ b/modules/private/websites/tools/tools/dmarc_reports/default.css | |||
@@ -1,126 +1,130 @@ | |||
1 | h1 { | 1 | h1 { |
2 | text-align: center; | 2 | text-align: center; |
3 | } | 3 | } |
4 | 4 | ||
5 | table.reportlist { | 5 | p.warninginfo { |
6 | margin: 2em auto 2em auto; | 6 | text-align: center; |
7 | border-collapse: collapse; | 7 | } |
8 | clear: both; | 8 | |
9 | } | 9 | table.reportlist { |
10 | 10 | margin: 2em auto 2em auto; | |
11 | table.reportlist td, table.reportlist th { | 11 | border-collapse: collapse; |
12 | padding:3px; | 12 | clear: both; |
13 | } | 13 | } |
14 | 14 | ||
15 | table.reportlist thead { | 15 | table.reportlist td, table.reportlist th { |
16 | border-top: 1px solid grey; | 16 | padding:3px; |
17 | border-bottom: 1px solid grey; | 17 | } |
18 | 18 | ||
19 | } | 19 | table.reportlist thead { |
20 | table.reportlist tbody tr:first-child td { | 20 | border-top: 1px solid grey; |
21 | padding-top: 10px; | 21 | border-bottom: 1px solid grey; |
22 | } | 22 | |
23 | table.reportlist tr.sum { | 23 | } |
24 | border-top: 1px solid grey; | 24 | table.reportlist tbody tr:first-child td { |
25 | } | 25 | padding-top: 10px; |
26 | table.reportlist tr.selected { | 26 | } |
27 | background-color: lightgrey; | 27 | table.reportlist tr.sum { |
28 | } | 28 | border-top: 1px solid grey; |
29 | .reportdesc { | 29 | } |
30 | font-weight: bold; | 30 | table.reportlist tr.selected { |
31 | width: 90%; | 31 | background-color: lightgrey; |
32 | margin-left: auto; | 32 | } |
33 | margin-right: auto; | 33 | .reportdesc { |
34 | } | 34 | font-weight: bold; |
35 | 35 | width: 90%; | |
36 | tr.summaryrow { | 36 | margin-left: auto; |
37 | cursor: pointer; | 37 | margin-right: auto; |
38 | } | 38 | } |
39 | 39 | ||
40 | tr.summaryrow:hover, tr.summaryrow.selected { | 40 | tr.summaryrow { |
41 | background-color: lightgray; | 41 | cursor: pointer; |
42 | border-left: 1px solid lightgray; | 42 | } |
43 | } | 43 | |
44 | 44 | tr.summaryrow:hover, tr.summaryrow.selected { | |
45 | td.reportcell { | 45 | background-color: lightgray; |
46 | border-bottom: 1px solid lightgray; | 46 | border-left: 1px solid lightgray; |
47 | border-left: 1px solid lightgray; | 47 | } |
48 | border-right: 1px solid lightgray; | 48 | |
49 | } | 49 | td.reportcell { |
50 | 50 | border-bottom: 1px solid lightgray; | |
51 | table.reportdata { | 51 | border-left: 1px solid lightgray; |
52 | margin: 0px auto 0px auto; | 52 | border-right: 1px solid lightgray; |
53 | border-collapse: separate; | 53 | } |
54 | border-spacing: 2px; | 54 | |
55 | } | 55 | table.reportdata { |
56 | 56 | margin: 0px auto 0px auto; | |
57 | table.reportdata tr th, table.reportdata tr td { | 57 | border-collapse: separate; |
58 | text-align: center; | 58 | border-spacing: 2px; |
59 | padding: 3px; | 59 | } |
60 | } | 60 | |
61 | 61 | table.reportdata tr th, table.reportdata tr td { | |
62 | table.reportdata tr.red { | 62 | text-align: center; |
63 | background-color: #FF0000; | 63 | padding: 3px; |
64 | } | 64 | } |
65 | 65 | ||
66 | table.reportdata tr.orange { | 66 | table.reportdata tr.red { |
67 | background-color: #FFA500; | 67 | background-color: #FF0000; |
68 | } | 68 | } |
69 | 69 | ||
70 | table.reportdata tr.lime { | 70 | table.reportdata tr.orange { |
71 | background-color: #00FF00; | 71 | background-color: #FFA500; |
72 | } | 72 | } |
73 | 73 | ||
74 | table.reportdata tr.yellow { | 74 | table.reportdata tr.lime { |
75 | background-color: #FFFF00; | 75 | background-color: #00FF00; |
76 | } | 76 | } |
77 | 77 | ||
78 | .optionblock { | 78 | table.reportdata tr.yellow { |
79 | background: lightgrey; | 79 | background-color: #FFFF00; |
80 | padding: 0.4em; | 80 | } |
81 | float: right; | 81 | |
82 | margin: auto 2em 1em auto; | 82 | .optionblock { |
83 | white-space: nowrap; | 83 | background: lightgrey; |
84 | } | 84 | padding: 0.4em; |
85 | 85 | float: right; | |
86 | .optionlabel { | 86 | margin: auto 2em 1em auto; |
87 | font-weight: bold; | 87 | white-space: nowrap; |
88 | float: left; clear: left; | 88 | } |
89 | margin-right: 1em; | 89 | |
90 | } | 90 | .optionlabel { |
91 | 91 | font-weight: bold; | |
92 | .options { | 92 | float: left; clear: left; |
93 | font-size: 70%; | 93 | margin-right: 1em; |
94 | text-align: right; | 94 | } |
95 | border: none; | 95 | |
96 | width: 97%; | 96 | .options { |
97 | padding: 0.4em; | 97 | font-size: 70%; |
98 | } | 98 | text-align: right; |
99 | 99 | border: none; | |
100 | .center { | 100 | width: 97%; |
101 | text-align:center; | 101 | padding: 0.4em; |
102 | } | 102 | } |
103 | 103 | ||
104 | .circle_lime:before { | 104 | .center { |
105 | content: ' \25CF'; | 105 | text-align:center; |
106 | font-size: 25px; | 106 | } |
107 | color: #00FF00; | 107 | |
108 | } | 108 | .circle_lime:before { |
109 | 109 | content: ' \25CF'; | |
110 | .circle_red:before { | 110 | font-size: 25px; |
111 | content: ' \25CF'; | 111 | color: #00FF00; |
112 | font-size: 25px; | 112 | } |
113 | color: #FF0000; | 113 | |
114 | } | 114 | .circle_red:before { |
115 | 115 | content: ' \25CF'; | |
116 | .circle_yellow:before { | 116 | font-size: 25px; |
117 | content: ' \25CF'; | 117 | color: #FF0000; |
118 | font-size: 25px; | 118 | } |
119 | color: #FFFF00; | 119 | |
120 | } | 120 | .circle_yellow:before { |
121 | 121 | content: ' \25CF'; | |
122 | .circle_orange:before { | 122 | font-size: 25px; |
123 | content: ' \25CF'; | 123 | color: #FFFF00; |
124 | font-size: 25px; | 124 | } |
125 | color: #FFA500; | 125 | |
126 | } | 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 index ded05e1..af29cdf 100644 --- a/modules/private/websites/tools/tools/dmarc_reports/index.html +++ b/modules/private/websites/tools/tools/dmarc_reports/index.html | |||
@@ -11,7 +11,13 @@ | |||
11 | 11 | ||
12 | <body> | 12 | <body> |
13 | <div id="app" style="width: 100%"> | 13 | <div id="app" style="width: 100%"> |
14 | <div class="optionblock" v-if="info"> | 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"> | ||
15 | <div class='options'> | 21 | <div class='options'> |
16 | <span class='optionlabel'>Hide all-green lines:</span> | 22 | <span class='optionlabel'>Hide all-green lines:</span> |
17 | <label><input type="radio" :value="false" v-model="filterGreen"> no</label> | 23 | <label><input type="radio" :value="false" v-model="filterGreen"> no</label> |
@@ -43,9 +49,13 @@ | |||
43 | <option v-for="date in info.dates" :value="date">{{ date }}</option> | 49 | <option v-for="date in info.dates" :value="date">{{ date }}</option> |
44 | </select> | 50 | </select> |
45 | </div> | 51 | </div> |
52 | </template> | ||
46 | </div> | 53 | </div> |
47 | 54 | ||
48 | <h1 class='main'>DMARC Reports</h1> | 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> | ||
49 | <table class='reportlist' v-if="summaries"> | 59 | <table class='reportlist' v-if="summaries"> |
50 | <thead> | 60 | <thead> |
51 | <tr> | 61 | <tr> |