Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Noreply shape check and default dkim selector #40

Merged
merged 4 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion classes/check/dnsmx.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ public function get_result() : result {
$status = result::ERROR;
$summary = "MX DNS record missing";
} else {
$allmxdomains = join('<br>', array_map(fn($x) => $x['target'] . ' (' . $x['pri'] . ')', $mxdomains));
$allmxdomains = join('<br>', array_map(function($x) {
return $x['target'] . ' (' . $x['pri'] . ')';
}, $mxdomains));
$details .= "<p>MX record found on domain <code>$noreplydomain</code> pointing to<br><code>$allmxdomains</code></p>";
$status = result::OK;
$summary = "MX record points to " . $mxdomains[0]['target'];
Expand Down
96 changes: 96 additions & 0 deletions classes/check/dnsnoreply.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* DNS Email Noreply check.
*
* @package tool_emailutils
* @author Benjamin Walker <[email protected]>
* @copyright Catalyst IT 2024
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*
*/

namespace tool_emailutils\check;
use core\check\check;
use core\check\result;
use tool_emailutils\dns_util;

/**
* DNS Email Noreply check.
*
* @package tool_emailutils
* @author Benjamin Walker <[email protected]>
* @copyright Catalyst IT 2024
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class dnsnoreply extends check {

/**
* A link to a place to action this
*
* @return \action_link|null
*/
public function get_action_link(): ?\action_link {
return new \action_link(
new \moodle_url('/admin/settings.php?section=outgoingmailconfig'),
get_string('outgoingmailconfig', 'core_admin'));
}

/**
* Get Result.
*
* @return result
*/
public function get_result() : result {
global $DB, $CFG;

$url = new \moodle_url($CFG->wwwroot);
$domain = $url->get_host();

$details = '';
$status = result::INFO;
$summary = '';

$dns = new dns_util();

$noreply = $dns->get_noreply();
$details .= "<p>No reply email: <code>$noreply</code></p>";

$noreplydomain = $dns->get_noreply_domain();
$details .= "<p>No reply domain: <code>$noreplydomain</code></p>";

$details .= "<p>LMS domain: <code>$domain</code></p>";

$primarydomain = $dns->get_primary_domain($domain);

if ($noreplydomain == $domain) {
$status = result::OK;
$summary = "LMS is same as noreply domain";
} else if (str_contains($domain, '.' . $noreplydomain)) {
$status = result::OK;
$summary = "LMS is a subdomain of noreply domain";
} else if (str_contains($noreplydomain, '.' . $domain)) {
$status = result::OK;
$summary = "Noreply domain is a subdomain of LMS";
} else if ($noreply == $primarydomain || str_contains($noreplydomain, '.' . $primarydomain)) {
$summary = "LMS and noreply domain have a shared domain";
} else {
$summary = "LMS and noreply domain have nothing in common";
}

return new result($status, $summary, $details);
}
}
96 changes: 92 additions & 4 deletions classes/dns_util.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,47 @@ public function get_noreply_domain() {
return $noreplydomain;
}

/**
* Attempts to extract the primary domain from a domain
*
* This may not always be clear, if there is any confusion return the full domain.
* @param string $domain domain to check
* @return string primary domain
*/
public function get_primary_domain($domain) {
$originaldomain = $domain;

// Checks for the first domain that has a NS record.
while ($domain) {
$records = @dns_get_record($domain, DNS_NS);
if (!empty($records)) {
return $domain;
}
$parts = explode('.', $domain);
// A domain should always have more than 1 part.
if (count($parts) >= 2) {
break;
}
$domain = join('.', array_slice($parts, 1));
}
return $originaldomain;
}

/**
* Attempts to extract the subdomains from a domain
*
* This may not always be clear, if there is any confusion return known subdomains or a blank string.
* @param string $domain domain to check
* @return string subdomains
*/
public function get_subdomains($domain) {
$primarydomain = $this->get_primary_domain($domain);
return rtrim(strstr($domain, $primarydomain, true), '.');
}

/**
* Get spf txt record contents
* @param string $domain specify a different domain
* @return string txt record
*/
public function get_spf_record($domain = '') {
Expand Down Expand Up @@ -96,7 +135,7 @@ public function get_mxtoolbox_spf_url() {
* Returns the include if matched
*
* The include can have a wildcard and this will return the actual matched value.
* @param string include domain
* @param string $include include domain
* @return string matched include
*/
public function include_present(string $include) {
Expand Down Expand Up @@ -125,6 +164,8 @@ public function get_dkim_selector() {

/**
* Get DKIM txt record contents
* @param string $selector DKIM selector
* @param string $domain DKIM domain
* @return string txt record
*/
public function get_dkim_dns_domain($selector, $domain) {
Expand All @@ -133,6 +174,7 @@ public function get_dkim_dns_domain($selector, $domain) {

/**
* Get DKIM txt record contents
* @param string $selector DKIM selector
* @return string txt record
*/
public function get_dkim_record($selector) {
Expand All @@ -149,7 +191,7 @@ public function get_dkim_record($selector) {

/**
* Get DKIM txt record contents
* @return string txt record
* @return array txt record
*/
public function get_dmarc_dns_record() {
$domain = $this->get_noreply_domain();
Expand Down Expand Up @@ -177,13 +219,14 @@ public function get_dmarc_dns_record() {

/**
* Get MX record contents
* @return string txt record
* @param string $domain domain to check
* @return array txt record
*/
public function get_mx_record($domain) {

$records = @dns_get_record($domain, DNS_MX);
if (empty($records)) {
return;
return [];
}
usort($records, function($a, $b) {
if ($a['pri'] == $b['pri']) {
Expand All @@ -196,6 +239,8 @@ public function get_mx_record($domain) {

/**
* Get matching record contents
* @param string $domain domain to check
* @param string $match search for specific match
* @return string txt record
*/
public function get_matching_dns_record($domain, $match) {
Expand All @@ -211,5 +256,48 @@ public function get_matching_dns_record($domain, $match) {
}
return '';
}

/**
* Gets the selector suffix
* @param string $domain check specific domain
* @return string suffix
*/
public function get_selector_suffix($domain = '') {
GLOBAL $CFG;

if (empty($domain)) {
$url = new \moodle_url($CFG->wwwroot);
$domain = $url->get_host();
}

// Determine the suffix based on the LMS domain and noreply domain.
$primarydomain = $this->get_primary_domain($domain);
$noreplydomain = $this->get_noreply_domain();
if ($primarydomain == $noreplydomain) {
// Noreply domain is same as primary domain, add all LMS subdomains.
$suffix = $this->get_subdomains($domain);
} else if (str_contains($domain, '.' . $noreplydomain)) {
// Noreply domain includes part of the LMS subdomain, only add different subdomains.
$suffix = str_replace('.' . $noreplydomain, '', $domain);
} else if (str_contains($noreplydomain, '.' . $domain)) {
// Noreply domain is a subdomain of LMS, domain already has all info.
$suffix = '';
} else if (str_contains($noreplydomain, '.' . $primarydomain)) {
// Noreply domain is a different subdomain of primary domain, add all LMS subdomains.
$suffix = $this->get_subdomains($domain);
} else {
// Noreply domain shares nothing in common with LMS, add entire LMS domain.
$suffix = $domain;
}

// Clean the suffix to remove www and foreign language chars, and convert '.' to '-'.
// Email filter is enough because domains don't contain the other allowed chars.
$suffix = ltrim($suffix, 'www.');
$suffix = trim(filter_var($suffix, FILTER_SANITIZE_EMAIL), '.');
$suffix = str_replace('.', '-', $suffix);

return $suffix;
}

}

20 changes: 19 additions & 1 deletion classes/form/create_dkim.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public function definition() {

$group[] =& $mform->createElement('text', 'selector', array("size" => 20));

$selector = \userdate(time(), get_string('selectordefault', 'tool_emailutils'));
$selector = $this->get_default_selector();
$mform->setDefault("selector", $selector);
$mform->setType('selector', PARAM_HOST);

Expand All @@ -74,4 +74,22 @@ public function validation($data, $files) {
$errors = parent::validation($data, $files);
return $errors;
}

/**
* Gets a selector value to use as a default
*
* @return string default selector
*/
private function get_default_selector() {
// Add date to default.
$selector = \userdate(time(), get_string('selectordefault', 'tool_emailutils'));

// Add suffix.
$dns = new \tool_emailutils\dns_util();
if ($suffix = $dns->get_selector_suffix()) {
$selector .= '-' . $suffix;
}
return $selector;
}

}
1 change: 1 addition & 0 deletions lang/en/tool_emailutils.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
</ol>
';
$string['checkdnsmx'] = 'DNS Email MX check';
$string['checkdnsnoreply'] = 'DNS Email noreply shape check';
$string['dnssettings'] = 'SPF / DKIM / DMARC DNS settings';
$string['dnsspfinclude'] = 'SPF include';
$string['dnsspfinclude_help'] = '<p>This is an SPF include domain which is expected to be present in the record. For example if this was set to <code>spf.acme.org</code> then the SPF security check would pass if the SPF record was <code>v=spf1 include:spf.acme.org -all</code>.</p>
Expand Down
1 change: 1 addition & 0 deletions lib.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ function tool_emailutils_security_checks() {
new \tool_emailutils\check\dnsdkim(),
new \tool_emailutils\check\dnsdmarc(),
new \tool_emailutils\check\dnsmx(),
new \tool_emailutils\check\dnsnoreply(),
new \tool_emailutils\check\dnspostmastertools(),
];
}
Expand Down
Loading
Loading