aboutsummaryrefslogtreecommitdiff
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
parenta8ef1adb4a90c2524ac09a85463598e5d41d2a4a (diff)
downloadNix-7df5e532c1ce2ab9e8527615c08c1178990870e6.tar.gz
Nix-7df5e532c1ce2ab9e8527615c08c1178990870e6.tar.zst
Nix-7df5e532c1ce2ab9e8527615c08c1178990870e6.zip
Add dmarc reports
-rw-r--r--modules/private/environment.nix8
-rw-r--r--modules/private/websites/tools/tools/default.nix16
-rw-r--r--modules/private/websites/tools/tools/dmarc_reports.nix56
-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
-rw-r--r--modules/private/websites/tools/tools/landing/config.yml3
8 files changed, 500 insertions, 0 deletions
diff --git a/modules/private/environment.nix b/modules/private/environment.nix
index f7994a1..5fbd023 100644
--- a/modules/private/environment.nix
+++ b/modules/private/environment.nix
@@ -820,6 +820,14 @@ in
820 }; 820 };
821 }; 821 };
822 }; 822 };
823 dmarc_reports = mkOption {
824 description = "DMARC reports configuration";
825 type = submodule {
826 options = {
827 mysql = mkMysqlOptions "DMARC" {};
828 };
829 };
830 };
823 etherpad-lite = mkOption { 831 etherpad-lite = mkOption {
824 description = "Etherpad configuration"; 832 description = "Etherpad configuration";
825 type = submodule { 833 type = submodule {
diff --git a/modules/private/websites/tools/tools/default.nix b/modules/private/websites/tools/tools/default.nix
index 0cb7a10..a5e7f2e 100644
--- a/modules/private/websites/tools/tools/default.nix
+++ b/modules/private/websites/tools/tools/default.nix
@@ -47,6 +47,9 @@ let
47 webhooks = pkgs.callPackage ./webhooks.nix { 47 webhooks = pkgs.callPackage ./webhooks.nix {
48 env = config.myEnv.tools.webhooks; 48 env = config.myEnv.tools.webhooks;
49 }; 49 };
50 dmarc-reports = pkgs.callPackage ./dmarc_reports.nix {
51 env = config.myEnv.tools.dmarc_reports;
52 };
50 53
51 landing = pkgs.callPackage ./landing.nix {}; 54 landing = pkgs.callPackage ./landing.nix {};
52 55
@@ -65,6 +68,7 @@ in {
65 ++ ttrss.keys 68 ++ ttrss.keys
66 ++ wallabag.keys 69 ++ wallabag.keys
67 ++ yourls.keys 70 ++ yourls.keys
71 ++ dmarc-reports.keys
68 ++ webhooks.keys; 72 ++ webhooks.keys;
69 73
70 services.duplyBackup.profiles = { 74 services.duplyBackup.profiles = {
@@ -88,6 +92,7 @@ in {
88 ++ rompr.apache.modules 92 ++ rompr.apache.modules
89 ++ shaarli.apache.modules 93 ++ shaarli.apache.modules
90 ++ dokuwiki.apache.modules 94 ++ dokuwiki.apache.modules
95 ++ dmarc-reports.apache.modules
91 ++ phpbb.apache.modules 96 ++ phpbb.apache.modules
92 ++ ldap.apache.modules 97 ++ ldap.apache.modules
93 ++ kanboard.apache.modules; 98 ++ kanboard.apache.modules;
@@ -147,6 +152,7 @@ in {
147 (kanboard.apache.vhostConf pcfg.kanboard.socket) 152 (kanboard.apache.vhostConf pcfg.kanboard.socket)
148 (grocy.apache.vhostConf pcfg.grocy.socket) 153 (grocy.apache.vhostConf pcfg.grocy.socket)
149 (phpbb.apache.vhostConf pcfg.phpbb.socket) 154 (phpbb.apache.vhostConf pcfg.phpbb.socket)
155 (dmarc-reports.apache.vhostConf pcfg.dmarc-reports.socket)
150 '' 156 ''
151 Alias /paste /var/lib/fiche 157 Alias /paste /var/lib/fiche
152 <Directory "/var/lib/fiche"> 158 <Directory "/var/lib/fiche">
@@ -342,6 +348,15 @@ in {
342 group = "wwwrun"; 348 group = "wwwrun";
343 settings = shaarli.phpFpm.pool; 349 settings = shaarli.phpFpm.pool;
344 }; 350 };
351 dmarc-reports = {
352 user = "wwwrun";
353 group = "wwwrun";
354 settings = dmarc-reports.phpFpm.pool;
355 phpEnv = dmarc-reports.phpFpm.phpEnv;
356 phpOptions = config.services.phpfpm.phpOptions + ''
357 extension=${pkgs.php}/lib/php/extensions/mysqli.so
358 '';
359 };
345 dokuwiki = { 360 dokuwiki = {
346 user = "wwwrun"; 361 user = "wwwrun";
347 group = "wwwrun"; 362 group = "wwwrun";
@@ -386,6 +401,7 @@ in {
386 401
387 services.websites.webappDirs = { 402 services.websites.webappDirs = {
388 _adminer = adminer.webRoot; 403 _adminer = adminer.webRoot;
404 "${dmarc-reports.apache.webappName}" = dmarc-reports.webRoot;
389 "${dokuwiki.apache.webappName}" = dokuwiki.webRoot; 405 "${dokuwiki.apache.webappName}" = dokuwiki.webRoot;
390 "${phpbb.apache.webappName}" = phpbb.webRoot; 406 "${phpbb.apache.webappName}" = phpbb.webRoot;
391 "${ldap.apache.webappName}" = "${ldap.webRoot}/htdocs"; 407 "${ldap.apache.webappName}" = "${ldap.webRoot}/htdocs";
diff --git a/modules/private/websites/tools/tools/dmarc_reports.nix b/modules/private/websites/tools/tools/dmarc_reports.nix
new file mode 100644
index 0000000..2e44526
--- /dev/null
+++ b/modules/private/websites/tools/tools/dmarc_reports.nix
@@ -0,0 +1,56 @@
1{ env }:
2rec {
3 keys = [{
4 dest = "webapps/tools-dmarc-reports.php";
5 user = "wwwrun";
6 group = "wwwrun";
7 permissions = "0400";
8 text = ''
9 <?php
10 $dbhost = "${env.mysql.host}";
11 $dbname = "${env.mysql.database}";
12 $dbuser = "${env.mysql.user}";
13 $dbpass = "${env.mysql.password}";
14 $dbport = "${env.mysql.port}";
15 ?>
16 '';
17 }];
18 webRoot = ./dmarc_reports;
19 apache = rec {
20 user = "wwwrun";
21 group = "wwwrun";
22 modules = [ "proxy_fcgi" ];
23 webappName = "tools_dmarc_reports";
24 root = "/run/current-system/webapps/${webappName}";
25 vhostConf = socket: ''
26 Alias /dmarc-reports "${root}"
27 <Directory "${root}">
28 DirectoryIndex index.html
29 <FilesMatch "\.php$">
30 SetHandler "proxy:unix:${socket}|fcgi://localhost"
31 </FilesMatch>
32
33 AllowOverride None
34 Options +FollowSymlinks
35 Require all granted
36 </Directory>
37 '';
38 };
39 phpFpm = rec {
40 basedir = builtins.concatStringsSep ":"
41 [ webRoot "/var/secrets/webapps/tools-dmarc-reports.php" ];
42 pool = {
43 "listen.owner" = apache.user;
44 "listen.group" = apache.group;
45 "pm" = "ondemand";
46 "pm.max_children" = "60";
47 "pm.process_idle_timeout" = "60";
48
49 # Needed to avoid clashes in browser cookies (same domain)
50 "php_admin_value[open_basedir]" = "${basedir}:/tmp";
51 };
52 phpEnv = {
53 SECRETS_FILE = "/var/secrets/webapps/tools-dmarc-reports.php";
54 };
55 };
56}
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>
diff --git a/modules/private/websites/tools/tools/landing/config.yml b/modules/private/websites/tools/tools/landing/config.yml
index 42ebcd1..f97b3f3 100644
--- a/modules/private/websites/tools/tools/landing/config.yml
+++ b/modules/private/websites/tools/tools/landing/config.yml
@@ -13,6 +13,9 @@ links:
13 icon: "fas fa-desktop" 13 icon: "fas fa-desktop"
14 url: "https://status.immae.eu" 14 url: "https://status.immae.eu"
15 target: '_blank' # optionnal html a tag target attribute 15 target: '_blank' # optionnal html a tag target attribute
16 - name: "DMARC status"
17 url: "https://tools.immae.eu/dmarc-reports"
18 target: '_blank'
16 - name: "Change password" 19 - name: "Change password"
17 url: "https://tools.immae.eu/ldap_password.php" 20 url: "https://tools.immae.eu/ldap_password.php"
18 target: '_blank' 21 target: '_blank'