From 4d9a9480bf417619242d52f18280fadadcc3db5e Mon Sep 17 00:00:00 2001 From: Shane Frasier Date: Wed, 22 Nov 2017 15:28:46 -0500 Subject: [PATCH 01/14] Renamed the "Errors" output field to "Debug" --- trustymail/domain.py | 6 +++--- trustymail/trustymail.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/trustymail/domain.py b/trustymail/domain.py index b0e5ff2..d2d4df8 100755 --- a/trustymail/domain.py +++ b/trustymail/domain.py @@ -47,8 +47,8 @@ def __init__(self, domain_name, timeout, smtp_timeout, smtp_localhost, smtp_port # 3. Whether or not the server supports STARTTLS self.starttls_results = {} - # A list of any errors that occurred while scanning records. - self.errors = [] + # A list of any debugging information collected while scanning records. + self.debug = [] # A list of the ports tested for SMTP self.ports_tested = set() @@ -144,7 +144,7 @@ def generate_results(self): "DMARC Policy": self.get_dmarc_policy(), "Syntax Errors": self.format_list(self.syntax_errors), - "Errors": self.format_list(self.errors) + "Debug": self.format_list(self.debug) } return results diff --git a/trustymail/trustymail.py b/trustymail/trustymail.py index 6231f49..7362f8e 100755 --- a/trustymail/trustymail.py +++ b/trustymail/trustymail.py @@ -21,7 +21,7 @@ "DMARC Record", "Valid DMARC", "DMARC Results", "DMARC Record on Base Domain", "Valid DMARC Record on Base Domain", "DMARC Results on Base Domain", "DMARC Policy", - "Syntax Errors", "Errors" + "Syntax Errors", "Debug" ] # A cache for SMTP scanning results @@ -226,7 +226,7 @@ def spf_scan(resolver, domain): else: domain.valid_spf = False logging.debug("\tResult Differs: Expected [{0}] - Actual [{1}]".format(result, response[0])) - domain.errors.append("Result Differs: Expected [{0}] - Actual [{1}]".format(result, response[0])) + domain.debug.append("Result Differs: Expected [{0}] - Actual [{1}]".format(result, response[0])) except (dns.resolver.NoNameservers, dns.resolver.NoAnswer, dns.exception.Timeout, dns.resolver.NXDOMAIN) as error: handle_error("[SPF]", domain, error) @@ -342,10 +342,10 @@ def handle_error(prefix, domain, error): if hasattr(error, "message"): if "NXDOMAIN" in error.message and prefix != "[DMARC]": domain.is_live = False - domain.errors.append(error.message) + domain.debug.append(error.message) logging.debug(" {0} {1}".format(prefix, error.message)) else: - domain.errors.append(str(error)) + domain.debug.append(str(error)) logging.debug(" {0} {1}".format(prefix, str(error))) From 8e3c80b7ac836ed23a4df48010f624fd40b846ab Mon Sep 17 00:00:00 2001 From: Shane Frasier Date: Fri, 24 Nov 2017 10:50:33 -0500 Subject: [PATCH 02/14] Updated README.md with new "Debug" name for what used to be the "Errors" field --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b28b7a5..b7aaecf 100644 --- a/README.md +++ b/README.md @@ -140,8 +140,9 @@ The following values are returned in `results.csv`: * `Syntax Errors` - A list of syntax errors that were detected when scanning DMARC or SPF records, or checking for STARTTLS support. -* `Errors` - A list of any other errors encountered, such as DNS - failures. +* `Debug` - A list of any other warnings encountered, such as DNS + failures. These can be helpful when debugging how `trustymail` + reached its conclusions. ## Public domain From 8173729a1df947bc59929065ecff4ab47e62f5ea Mon Sep 17 00:00:00 2001 From: Shane Frasier Date: Tue, 28 Nov 2017 14:40:19 -0500 Subject: [PATCH 03/14] Previously there was only logging output when the --debug flag was present. I changed it so that the warn and error messages will still be output even if the --debug is not present. --- trustymail/cli.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/trustymail/cli.py b/trustymail/cli.py index 4256352..6c81cb6 100755 --- a/trustymail/cli.py +++ b/trustymail/cli.py @@ -37,10 +37,10 @@ """ from trustymail import __version__ -import logging import docopt -import os import errno +import logging +import os from trustymail import trustymail @@ -51,8 +51,10 @@ def main(): args = docopt.docopt(__doc__, version=__version__) + log_level = logging.WARN if args['--debug']: - logging.basicConfig(format='%(message)s', level=logging.DEBUG) + log_level = logging.DEBUG + logging.basicConfig(format='%(asctime)-15s %(message)s', level=log_level) # Allow for user to input a csv for many domain names. if args['INPUT'][0].endswith('.csv'): From cc6d3f8b8aaf8ba21b72145c01f5ab5147ea1b8e Mon Sep 17 00:00:00 2001 From: Shane Frasier Date: Tue, 28 Nov 2017 14:57:41 -0500 Subject: [PATCH 04/14] Revamped the logging. All logging is now done through handle_error and handle_syntax error. In addition, log output now includes the function name, filename, and line number from where the logging information originated. The logging information that is printed to the console also includes a timestamp. --- trustymail/trustymail.py | 62 ++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/trustymail/trustymail.py b/trustymail/trustymail.py index de67b2e..a7a2dda 100755 --- a/trustymail/trustymail.py +++ b/trustymail/trustymail.py @@ -1,5 +1,6 @@ import csv import datetime +import inspect import json import logging import re @@ -72,8 +73,7 @@ def mx_scan(resolver, domain): def starttls_scan(domain, smtp_timeout, smtp_localhost, smtp_ports, smtp_cache): - """ - Scan a domain to see if it supports SMTP and supports STARTTLS. + """Scan a domain to see if it supports SMTP and supports STARTTLS. Scan a domain to see if it supports SMTP. If the domain does support SMTP, a further check will be done to see if it supports STARTTLS. @@ -169,8 +169,7 @@ def starttls_scan(domain, smtp_timeout, smtp_localhost, smtp_ports, smtp_cache): def check_spf_record(record_text, expected_result, domain): - """ - Test to see if an SPF record is valid and correct. + """Test to see if an SPF record is valid and correct. The record is tested by checking the response when we query if it allows us to send mail from an IP that is known not to be a mail @@ -196,32 +195,28 @@ def check_spf_record(record_text, expected_result, domain): # I'm actually temporarily using an IP that virginia.edu resolves to # until we resolve why Google DNS does not return the same PTR records # as the CAL DNS does for 64.69.57.18. - query = spf.query("128.143.22.36", "email_wizard@" + domain.domain_name, domain.domain_name, strict=2) + query = spf.query('128.143.22.36', 'email_wizard@' + domain.domain_name, domain.domain_name, strict=2) response = query.check() if response[0] == 'temperror': logging.debug(response[2]) elif response[0] == 'permerror': - logging.debug('\t' + response[2]) - domain.syntax_errors.append(response[2]) + handle_syntax_error('[SPF]', domain, response[2]) elif response[0] == 'ambiguous': - logging.debug('\t' + response[2]) - domain.syntax_errors.append(response[2]) + handle_syntax_error('[SPF]', domain, response[2]) elif response[0] == expected_result: # Everything checks out the SPF syntax seems valid. domain.valid_spf = True else: domain.valid_spf = False - logging.debug('\tResult Differs: Expected [{0}] - Actual [{1}]'.format(expected_result, response[0])) - domain.errors.append('Result Differs: Expected [{0}] - Actual [{1}]'.format(expected_result, response[0])) + msg = 'Result Differs: Expected [{0}] - Actual [{1}]'.format(expected_result, response[0]) + handle_error('[SPF]', domain, msg) except spf.AmbiguityWarning as error: - logging.debug('\t' + error.msg) - domain.syntax_errors.append(error.msg) + handle_syntax_error('[SPF]', domain, error) def get_spf_record_text(resolver, domain_name, domain, follow_redirect=False): - """ - Get the SPF record text for the given domain name. + """Get the SPF record text for the given domain name. DNS queries are performed using the dns.resolver.Resolver object. Errors are logged to the trustymail.Domain object. The Boolean @@ -271,8 +266,7 @@ def get_spf_record_text(resolver, domain_name, domain, follow_redirect=False): def spf_scan(resolver, domain): - """ - Scan a domain to see if it supports SPF. If the domain has an SPF + """Scan a domain to see if it supports SPF. If the domain has an SPF record, verify that it properly rejects mail sent from an IP known to be disallowed. @@ -339,7 +333,7 @@ def dmarc_scan(resolver, domain): for tag in tag_dict: if tag not in ["v", "mailto", "rf", "p", "sp", "adkim", "aspf", "fo", "pct", "ri", "rua", "ruf"]: - logging.debug("\tWarning: Unknown DMARC mechanism {0}".format(tag)) + handle_error('[DMARC]', domain, 'Warning: Unknown DMARC mechanism {0}'.format(tag)) domain.valid_dmarc = False elif tag == "p": domain.dmarc_policy = tag_dict[tag] @@ -415,15 +409,33 @@ def scan(domain_name, timeout, smtp_timeout, smtp_localhost, smtp_ports, smtp_ca return domain -def handle_error(prefix, domain, error): - if hasattr(error, "message"): - if "NXDOMAIN" in error.message and prefix != "[DMARC]": +def handle_error(prefix, domain, error, syntax_error=False): + # Get the previous frame in the stack - the one that is calling + # this function + frame = inspect.currentframe().f_back + function = frame.f_code + function_name = function.co_name + filename = function.co_filename + line = frame.f_lineno + + error_template = '{prefix} In {filename}:{line} in {function_name}: {error}' + + if hasattr(error, 'message'): + if syntax_error and 'NXDOMAIN' in error.message and prefix != '[DMARC]': domain.is_live = False - domain.errors.append(error.message) - logging.debug(" {0} {1}".format(prefix, error.message)) + error_string = error_template.format(prefix=prefix, function_name=function_name, line=line, filename=filename, error=error.message) + else: + error_string = error_template.format(prefix=prefix, function_name=function_name, line=line, filename=filename, error=str(error)) + + if syntax_error: + domain.syntax_errors.append(error_string) else: - domain.errors.append(str(error)) - logging.debug(" {0} {1}".format(prefix, str(error))) + domain.errors.append(error_string) + logging.debug(error_string) + + +def handle_syntax_error(prefix, domain, error): + handle_error(prefix, domain, error, syntax_error=True) def generate_csv(domains, file_name): From bf3c86d08622859188c6537f5700d4c452efb09d Mon Sep 17 00:00:00 2001 From: Shane Frasier Date: Tue, 28 Nov 2017 15:13:26 -0500 Subject: [PATCH 05/14] Fix flake8 error --- trustymail/domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trustymail/domain.py b/trustymail/domain.py index 5a5b657..4fb5651 100755 --- a/trustymail/domain.py +++ b/trustymail/domain.py @@ -144,7 +144,7 @@ def generate_results(self): "Syntax Errors": self.format_list(self.syntax_errors), "Debug": self.format_list(self.debug) - } + } return results From bfad46c2d446074efc3be1d44ee066da5a076258 Mon Sep 17 00:00:00 2001 From: Shane Frasier Date: Tue, 28 Nov 2017 15:15:12 -0500 Subject: [PATCH 06/14] Put the dev tag back in the version --- trustymail/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trustymail/__init__.py b/trustymail/__init__.py index 0404d81..2385e83 100644 --- a/trustymail/__init__.py +++ b/trustymail/__init__.py @@ -1 +1 @@ -__version__ = '0.3.0' +__version__ = '0.3.0-dev' From b364d55f297349743f73c66214812231a0ffce5f Mon Sep 17 00:00:00 2001 From: Shane Frasier Date: Tue, 28 Nov 2017 15:31:20 -0500 Subject: [PATCH 07/14] Big ugly commit wherein I changed double quotes to single quotes --- trustymail/cli.py | 8 ++-- trustymail/domain.py | 62 ++++++++++++++--------------- trustymail/trustymail.py | 86 ++++++++++++++++++++-------------------- 3 files changed, 78 insertions(+), 78 deletions(-) diff --git a/trustymail/cli.py b/trustymail/cli.py index 6c81cb6..ca741ca 100755 --- a/trustymail/cli.py +++ b/trustymail/cli.py @@ -93,10 +93,10 @@ def main(): # User might not want every scan performed. scan_types = { - "mx": args["--mx"], - "starttls": args["--starttls"], - "spf": args["--spf"], - "dmarc": args["--dmarc"] + 'mx': args['--mx'], + 'starttls': args['--starttls'], + 'spf': args['--spf'], + 'dmarc': args['--dmarc'] } domain_scans = [] diff --git a/trustymail/domain.py b/trustymail/domain.py index 4fb5651..321accb 100755 --- a/trustymail/domain.py +++ b/trustymail/domain.py @@ -17,7 +17,7 @@ def __init__(self, domain_name, timeout, smtp_timeout, smtp_localhost, smtp_port if self.base_domain_name != self.domain_name: if self.base_domain_name not in Domain.base_domains: # Populate DMARC for parent. - domain = trustymail.scan(self.base_domain_name, timeout, smtp_timeout, smtp_localhost, smtp_ports, smtp_cache, {"mx": False, "starttls": False, "spf": False, "dmarc": True}, dns_hostnames) + domain = trustymail.scan(self.base_domain_name, timeout, smtp_timeout, smtp_localhost, smtp_ports, smtp_cache, {'mx': False, 'starttls': False, 'spf': False, 'dmarc': True}, dns_hostnames) Domain.base_domains[self.base_domain_name] = domain self.base_domain = Domain.base_domains[self.base_domain_name] else: @@ -61,7 +61,7 @@ def has_supports_smtp(self): Returns True if any of the mail servers associated with this domain are listening and support SMTP. """ - return len(filter(lambda x: self.starttls_results[x]["supports_smtp"], + return len(filter(lambda x: self.starttls_results[x]['supports_smtp'], self.starttls_results.keys())) > 0 def has_starttls(self): @@ -69,7 +69,7 @@ def has_starttls(self): Returns True if any of the mail servers associated with this domain are listening and support STARTTLS. """ - return len(filter(lambda x: self.starttls_results[x]["starttls"], + return len(filter(lambda x: self.starttls_results[x]['starttls'], self.starttls_results.keys())) > 0 def has_spf(self): @@ -101,49 +101,49 @@ def parent_dmarc_results(self): def get_dmarc_policy(self): # If the policy was never set, or isn't in the list of valid policies, check the parents. - if self.dmarc_policy is None or self.dmarc_policy.lower() not in ["quarantine", "reject", "none"]: + if self.dmarc_policy is None or self.dmarc_policy.lower() not in ['quarantine', 'reject', 'none']: if self.base_domain is None: - return "" + return '' else: return self.base_domain.get_dmarc_policy() return self.dmarc_policy def generate_results(self): - mail_servers_that_support_smtp = [x for x in self.starttls_results.keys() if self.starttls_results[x]["supports_smtp"]] - mail_servers_that_support_starttls = [x for x in self.starttls_results.keys() if self.starttls_results[x]["starttls"]] + mail_servers_that_support_smtp = [x for x in self.starttls_results.keys() if self.starttls_results[x]['supports_smtp']] + mail_servers_that_support_starttls = [x for x in self.starttls_results.keys() if self.starttls_results[x]['starttls']] domain_supports_smtp = bool(mail_servers_that_support_starttls) results = { - "Domain": self.domain_name, - "Base Domain": self.base_domain_name, - "Live": self.is_live, - - "MX Record": self.has_mail(), - "Mail Servers": self.format_list(self.mail_servers), - "Mail Server Ports Tested": self.format_list([str(port) for port in self.ports_tested]), - "Domain Supports SMTP Results": self.format_list(mail_servers_that_support_smtp), + 'Domain': self.domain_name, + 'Base Domain': self.base_domain_name, + 'Live': self.is_live, + + 'MX Record': self.has_mail(), + 'Mail Servers': self.format_list(self.mail_servers), + 'Mail Server Ports Tested': self.format_list([str(port) for port in self.ports_tested]), + 'Domain Supports SMTP Results': self.format_list(mail_servers_that_support_smtp), # True if and only if at least one mail server speaks SMTP - "Domain Supports SMTP": domain_supports_smtp, - "Domain Supports STARTTLS Results": self.format_list(mail_servers_that_support_starttls), + 'Domain Supports SMTP': domain_supports_smtp, + 'Domain Supports STARTTLS Results': self.format_list(mail_servers_that_support_starttls), # True if and only if all mail servers that speak SMTP # also support STARTTLS - "Domain Supports STARTTLS": domain_supports_smtp and all([self.starttls_results[x]["starttls"] for x in mail_servers_that_support_smtp]), + 'Domain Supports STARTTLS': domain_supports_smtp and all([self.starttls_results[x]['starttls'] for x in mail_servers_that_support_smtp]), - "SPF Record": self.has_spf(), - "Valid SPF": self.valid_spf, - "SPF Results": self.format_list(self.spf), + 'SPF Record': self.has_spf(), + 'Valid SPF': self.valid_spf, + 'SPF Results': self.format_list(self.spf), - "DMARC Record": self.has_dmarc(), - "Valid DMARC": self.has_dmarc() and self.valid_dmarc, - "DMARC Results": self.format_list(self.dmarc), + 'DMARC Record': self.has_dmarc(), + 'Valid DMARC': self.has_dmarc() and self.valid_dmarc, + 'DMARC Results': self.format_list(self.dmarc), - "DMARC Record on Base Domain": self.parent_has_dmarc(), - "Valid DMARC Record on Base Domain": self.parent_has_dmarc() and self.parent_valid_dmarc(), - "DMARC Results on Base Domain": self.parent_dmarc_results(), - "DMARC Policy": self.get_dmarc_policy(), + 'DMARC Record on Base Domain': self.parent_has_dmarc(), + 'Valid DMARC Record on Base Domain': self.parent_has_dmarc() and self.parent_valid_dmarc(), + 'DMARC Results on Base Domain': self.parent_dmarc_results(), + 'DMARC Policy': self.get_dmarc_policy(), - "Syntax Errors": self.format_list(self.syntax_errors), - "Debug": self.format_list(self.debug) + 'Syntax Errors': self.format_list(self.syntax_errors), + 'Debug': self.format_list(self.debug) } return results @@ -157,4 +157,4 @@ def format_list(self, record_list): if not record_list: return None - return ", ".join(record_list) + return ', '.join(record_list) diff --git a/trustymail/trustymail.py b/trustymail/trustymail.py index e97c321..ff6041e 100755 --- a/trustymail/trustymail.py +++ b/trustymail/trustymail.py @@ -16,15 +16,15 @@ from trustymail.domain import Domain CSV_HEADERS = [ - "Domain", "Base Domain", "Live", - "MX Record", "Mail Servers", "Mail Server Ports Tested", - "Domain Supports SMTP", "Domain Supports SMTP Results", - "Domain Supports STARTTLS", "Domain Supports STARTTLS Results", - "SPF Record", "Valid SPF", "SPF Results", - "DMARC Record", "Valid DMARC", "DMARC Results", - "DMARC Record on Base Domain", "Valid DMARC Record on Base Domain", - "DMARC Results on Base Domain", "DMARC Policy", - "Syntax Errors", "Debug" + 'Domain', 'Base Domain', 'Live', + 'MX Record', 'Mail Servers', 'Mail Server Ports Tested', + 'Domain Supports SMTP', 'Domain Supports SMTP Results', + 'Domain Supports STARTTLS', 'Domain Supports STARTTLS Results', + 'SPF Record', 'Valid SPF', 'SPF Results', + 'DMARC Record', 'Valid DMARC', 'DMARC Results', + 'DMARC Record on Base Domain', 'Valid DMARC Record on Base Domain', + 'DMARC Results on Base Domain', 'DMARC Policy', + 'Syntax Errors', 'Debug' ] # A cache for SMTP scanning results @@ -49,7 +49,7 @@ def domain_list_from_csv(csv_file): for i in range(0, len(domain_list[0])): header = domain_list[0][i] - if "domain" in header.lower(): + if 'domain' in header.lower(): domain_column = i # CSV starts with headers, remove first row. domain_list.pop(0) @@ -69,7 +69,7 @@ def mx_scan(resolver, domain): for record in resolver.query(domain.domain_name, 'MX', tcp=True): domain.add_mx_record(record) except (dns.resolver.NoNameservers, dns.resolver.NoAnswer, dns.exception.Timeout, dns.resolver.NXDOMAIN) as error: - handle_error("[MX]", domain, error) + handle_error('[MX]', domain, error) def starttls_scan(domain, smtp_timeout, smtp_localhost, smtp_ports, smtp_cache): @@ -100,24 +100,24 @@ def starttls_scan(domain, smtp_timeout, smtp_localhost, smtp_ports, smtp_cache): for mail_server in domain.mail_servers: for port in smtp_ports: domain.ports_tested.add(port) - server_and_port = mail_server + ":" + str(port) + server_and_port = mail_server + ':' + str(port) if not smtp_cache or (server_and_port not in _SMTP_CACHE): domain.starttls_results[server_and_port] = {} smtp_connection = smtplib.SMTP(timeout=smtp_timeout, local_hostname=smtp_localhost) - logging.debug("Testing " + server_and_port + " for STARTTLS support") + logging.debug('Testing ' + server_and_port + ' for STARTTLS support') # Try to connect. This will tell us if something is # listening. try: smtp_connection.connect(mail_server, port) - domain.starttls_results[server_and_port]["is_listening"] = True + domain.starttls_results[server_and_port]['is_listening'] = True except (socket.timeout, smtplib.SMTPConnectError, smtplib.SMTPServerDisconnected, ConnectionRefusedError, OSError) as error: - handle_error("[STARTTLS]", domain, error) - domain.starttls_results[server_and_port]["is_listening"] = False - domain.starttls_results[server_and_port]["supports_smtp"] = False - domain.starttls_results[server_and_port]["starttls"] = False + handle_error('[STARTTLS]', domain, error) + domain.starttls_results[server_and_port]['is_listening'] = False + domain.starttls_results[server_and_port]['supports_smtp'] = False + domain.starttls_results[server_and_port]['starttls'] = False if smtp_cache: _SMTP_CACHE[server_and_port] = domain.starttls_results[server_and_port] @@ -128,18 +128,18 @@ def starttls_scan(domain, smtp_timeout, smtp_localhost, smtp_ports, smtp_cache): # thing that is listening is an SMTP server. try: smtp_connection.ehlo_or_helo_if_needed() - domain.starttls_results[server_and_port]["supports_smtp"] = True - logging.debug("\t Supports SMTP") + domain.starttls_results[server_and_port]['supports_smtp'] = True + logging.debug('\t Supports SMTP') except (smtplib.SMTPHeloError, smtplib.SMTPServerDisconnected) as error: - handle_error("[STARTTLS]", domain, error) - domain.starttls_results[server_and_port]["supports_smtp"] = False - domain.starttls_results[server_and_port]["starttls"] = False + handle_error('[STARTTLS]', domain, error) + domain.starttls_results[server_and_port]['supports_smtp'] = False + domain.starttls_results[server_and_port]['starttls'] = False # smtplib freaks out if you call quit on a non-open # connection try: smtp_connection.quit() except smtplib.SMTPServerDisconnected as error2: - handle_error("[STARTTLS]", domain, error2) + handle_error('[STARTTLS]', domain, error2) if smtp_cache: _SMTP_CACHE[server_and_port] = domain.starttls_results[server_and_port] @@ -147,9 +147,9 @@ def starttls_scan(domain, smtp_timeout, smtp_localhost, smtp_ports, smtp_cache): continue # Now check if the server supports STARTTLS. - has_starttls = smtp_connection.has_extn("STARTTLS") - domain.starttls_results[server_and_port]["starttls"] = has_starttls - logging.debug("\t Supports STARTTLS: " + str(has_starttls)) + has_starttls = smtp_connection.has_extn('STARTTLS') + domain.starttls_results[server_and_port]['starttls'] = has_starttls + logging.debug('\t Supports STARTTLS: ' + str(has_starttls)) # Close the connection # smtplib freaks out if you call quit on a non-open @@ -157,13 +157,13 @@ def starttls_scan(domain, smtp_timeout, smtp_localhost, smtp_ports, smtp_cache): try: smtp_connection.quit() except smtplib.SMTPServerDisconnected as error: - handle_error("[STARTTLS]", domain, error) + handle_error('[STARTTLS]', domain, error) # Copy the results into the cache, if necessary if smtp_cache: _SMTP_CACHE[server_and_port] = domain.starttls_results[server_and_port] else: - logging.debug("\tUsing cached results for " + server_and_port) + logging.debug('\tUsing cached results for ' + server_and_port) # Copy the cached results into the domain object domain.starttls_results[server_and_port] = _SMTP_CACHE[server_and_port] @@ -315,7 +315,7 @@ def dmarc_scan(resolver, domain): # Ensure the record is a DMARC record. Some domains that # redirect will cause an SPF record to show. - if record_text.startswith("v=DMARC1"): + if record_text.startswith('v=DMARC1'): domain.dmarc.append(record_text) # Remove excess whitespace @@ -324,28 +324,28 @@ def dmarc_scan(resolver, domain): # DMARC records follow a specific outline as to how they are defined - tag:value # We can split this up into a easily manipulatable tag_dict = {} - for options in record_text.split(";"): + for options in record_text.split(';'): if '=' not in options: continue - tag = options.split("=")[0].strip() - value = options.split("=")[1].strip() + tag = options.split('=')[0].strip() + value = options.split('=')[1].strip() tag_dict[tag] = value for tag in tag_dict: - if tag not in ["v", "mailto", "rf", "p", "sp", "adkim", "aspf", "fo", "pct", "ri", "rua", "ruf"]: + if tag not in ['v', 'mailto', 'rf', 'p', 'sp', 'adkim', 'aspf', 'fo', 'pct', 'ri', 'rua', 'ruf']: handle_error('[DMARC]', domain, 'Warning: Unknown DMARC mechanism {0}'.format(tag)) domain.valid_dmarc = False - elif tag == "p": + elif tag == 'p': domain.dmarc_policy = tag_dict[tag] except (dns.resolver.NoNameservers, dns.resolver.NoAnswer, dns.exception.Timeout, dns.resolver.NXDOMAIN) as error: - handle_error("[DMARC]", domain, error) + handle_error('[DMARC]', domain, error) def find_host_from_ip(resolver, ip_addr): # Use TCP, since we care about the content and correctness of the records # more than whether their records fit in a single UDP packet. - hostname, _ = resolver.query(dns.reversename.from_address(ip_addr), "PTR", tcp=True) + hostname, _ = resolver.query(dns.reversename.from_address(ip_addr), 'PTR', tcp=True) return str(hostname) @@ -385,22 +385,22 @@ def scan(domain_name, timeout, smtp_timeout, smtp_localhost, smtp_ports, smtp_ca # scan in its init domain = Domain(domain_name, timeout, smtp_timeout, smtp_localhost, smtp_ports, smtp_cache, dns_hostnames) - logging.debug("[{0}]".format(domain_name.lower())) + logging.debug('[{0}]'.format(domain_name.lower())) - if scan_types["mx"] and domain.is_live: + if scan_types['mx'] and domain.is_live: mx_scan(resolver, domain) - if scan_types["starttls"] and domain.is_live: + if scan_types['starttls'] and domain.is_live: starttls_scan(domain, smtp_timeout, smtp_localhost, smtp_ports, smtp_cache) - if scan_types["spf"] and domain.is_live: + if scan_types['spf'] and domain.is_live: spf_scan(resolver, domain) - if scan_types["dmarc"] and domain.is_live: + if scan_types['dmarc'] and domain.is_live: dmarc_scan(resolver, domain) # If the user didn't specify any scans then run a full scan. - if domain.is_live and not (scan_types["mx"] or scan_types["starttls"] or scan_types["spf"] or scan_types["dmarc"]): + if domain.is_live and not (scan_types['mx'] or scan_types['starttls'] or scan_types['spf'] or scan_types['dmarc']): mx_scan(resolver, domain) starttls_scan(domain, smtp_timeout, smtp_localhost, smtp_ports, smtp_cache) spf_scan(resolver, domain) From 24e82e3ce95cdd1de6446cab09a6a4bcbeda9fd6 Mon Sep 17 00:00:00 2001 From: Shane Frasier Date: Tue, 28 Nov 2017 17:00:43 -0500 Subject: [PATCH 08/14] Refined description of "Syntax Errors" and "Debug" fields --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index bb4aa81..206cd4c 100644 --- a/README.md +++ b/README.md @@ -143,11 +143,12 @@ The following values are returned in `results.csv`: #### Etc. -* `Syntax Errors` - A list of syntax errors that were detected when - scanning DMARC or SPF records, or checking for STARTTLS support. -* `Debug` - A list of any other warnings encountered, such as DNS - failures. These can be helpful when debugging how `trustymail` - reached its conclusions. +* `Syntax Errors` - A list of syntax errors that were encountered when + analyzing SPF records. +* `Debug` - A list of any other warnings or errors encountered, such + as DNS failures. These can be helpful when determining how + `trustymail` reached its conclusions, and are indispensible for bug + reports. ## Public domain From 88358013042c223574fb13a23aafe4a2651ce4f2 Mon Sep 17 00:00:00 2001 From: Shane Frasier Date: Tue, 28 Nov 2017 17:01:21 -0500 Subject: [PATCH 09/14] * Noticed that if the SPF query returns a "temperror" then a logging statement is made but nothing is stored in "Syntax Errors" or "Debug". I remedied that. * Further refinements to logging. Mostly cosmetic. --- trustymail/trustymail.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/trustymail/trustymail.py b/trustymail/trustymail.py index ff6041e..2b1c1c1 100755 --- a/trustymail/trustymail.py +++ b/trustymail/trustymail.py @@ -198,18 +198,15 @@ def check_spf_record(record_text, expected_result, domain): query = spf.query('128.143.22.36', 'email_wizard@' + domain.domain_name, domain.domain_name, strict=2) response = query.check() - if response[0] == 'temperror': - logging.debug(response[2]) - elif response[0] == 'permerror': - handle_syntax_error('[SPF]', domain, response[2]) - elif response[0] == 'ambiguous': - handle_syntax_error('[SPF]', domain, response[2]) - elif response[0] == expected_result: - # Everything checks out the SPF syntax seems valid. + response_type = response[0] + if response_type == 'temperror' or response_type == 'permerror' or response_type == 'ambiguous': + handle_error('[SPF]', domain, 'SPF query returned {}: {}'.format(response_type, response[2])) + elif response_type == expected_result: + # Everything checks out. The SPF syntax seems valid domain.valid_spf = True else: domain.valid_spf = False - msg = 'Result Differs: Expected [{0}] - Actual [{1}]'.format(expected_result, response[0]) + msg = 'Result unexpectedly differs: Expected [{}] - actual [{}]'.format(expected_result, response_type) handle_error('[SPF]', domain, msg) except spf.AmbiguityWarning as error: handle_syntax_error('[SPF]', domain, error) @@ -418,7 +415,7 @@ def handle_error(prefix, domain, error, syntax_error=False): filename = function.co_filename line = frame.f_lineno - error_template = '{prefix} In {filename}:{line} in {function_name}: {error}' + error_template = '{prefix} In {function_name} at {filename}:{line}: {error}' if hasattr(error, 'message'): if syntax_error and 'NXDOMAIN' in error.message and prefix != '[DMARC]': From 1b27451067b473e04ffe4a0f47bf99d387b9c824 Mon Sep 17 00:00:00 2001 From: Shane Frasier Date: Sat, 2 Dec 2017 14:08:48 -0500 Subject: [PATCH 10/14] Rearranged some imports --- trustymail/cli.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/trustymail/cli.py b/trustymail/cli.py index ca741ca..85903ae 100755 --- a/trustymail/cli.py +++ b/trustymail/cli.py @@ -35,13 +35,16 @@ Notes: If no scan type options are specified, all are run against a given domain/input. """ -from trustymail import __version__ - -import docopt +# Built-in imports import errno import logging import os +# Dependency imports +import docopt + +# Local imports +from trustymail import __version__ from trustymail import trustymail # The default ports to be checked to see if an SMTP server is listening. From 2dd1d076cf314ad6f7f3bf0f5940f4cbb715249c Mon Sep 17 00:00:00 2001 From: Shane Frasier Date: Sat, 2 Dec 2017 14:38:52 -0500 Subject: [PATCH 11/14] Added a docstring for the handle_error() method. --- trustymail/trustymail.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/trustymail/trustymail.py b/trustymail/trustymail.py index 2b1c1c1..0c69d05 100755 --- a/trustymail/trustymail.py +++ b/trustymail/trustymail.py @@ -407,6 +407,37 @@ def scan(domain_name, timeout, smtp_timeout, smtp_localhost, smtp_ports, smtp_ca def handle_error(prefix, domain, error, syntax_error=False): + """Handle an error by logging via the Python logging library and + recording it in the debug or syntax_error members of the + trustymail.Domain object. + + Since the "Debug" and "Syntax Error" fields in the CSV output of + trustymail come directly from the debug and syntax_error members + of the trustymail.Domain object, and that CSV is likely all we + will have to reconstruct how trustymail reached the conclusions it + did, it is vital to record as much helpful information as + possible. + + Parameters + ---------- + prefix : str + The prefix to use when constructing the log string. This is + usually the type of trustymail test that was being performed + when the error condition occurred. + + domain : trustymail.Domain + The Domain object in which the error or syntax error should be + recorded. + + error : str, BaseException, or Exception + Either a string describing the error, or an exception object + representing the error. + + syntax_error : bool + If True then the error will be recorded in the syntax_error + member of the trustymail.Domain object. Otherwise it is + recorded in the error member of the trustymail.Domain object. + """ # Get the previous frame in the stack - the one that is calling # this function frame = inspect.currentframe().f_back @@ -432,6 +463,9 @@ def handle_error(prefix, domain, error, syntax_error=False): def handle_syntax_error(prefix, domain, error): + """Convenience method for handle_error(prefix, domain, error, + syntax_error=True) + """ handle_error(prefix, domain, error, syntax_error=True) From bea61f20169e9ccb500e05d8e602c105f44a9586 Mon Sep 17 00:00:00 2001 From: Shane Frasier Date: Sat, 2 Dec 2017 14:44:14 -0500 Subject: [PATCH 12/14] Renamed debug to debug_info --- trustymail/cli.py | 2 +- trustymail/domain.py | 4 ++-- trustymail/trustymail.py | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/trustymail/cli.py b/trustymail/cli.py index 85903ae..45a33e1 100755 --- a/trustymail/cli.py +++ b/trustymail/cli.py @@ -22,7 +22,7 @@ --spf Only check spf records --dmarc Only check dmarc records --json Output is in json format (default csv) - --debug Output should include error messages. + --debug Output should include more verbose logging. --dns-hostnames=HOSTNAMES A comma-delimited list of DNS servers to query against. For example, if you want to use Google's DNS then you would use the diff --git a/trustymail/domain.py b/trustymail/domain.py index 321accb..393fd08 100755 --- a/trustymail/domain.py +++ b/trustymail/domain.py @@ -48,7 +48,7 @@ def __init__(self, domain_name, timeout, smtp_timeout, smtp_localhost, smtp_port self.starttls_results = {} # A list of any debugging information collected while scanning records. - self.debug = [] + self.debug_info = [] # A list of the ports tested for SMTP self.ports_tested = set() @@ -143,7 +143,7 @@ def generate_results(self): 'DMARC Policy': self.get_dmarc_policy(), 'Syntax Errors': self.format_list(self.syntax_errors), - 'Debug': self.format_list(self.debug) + 'Debug Info': self.format_list(self.debug_info) } return results diff --git a/trustymail/trustymail.py b/trustymail/trustymail.py index 0c69d05..a447d7c 100755 --- a/trustymail/trustymail.py +++ b/trustymail/trustymail.py @@ -24,7 +24,7 @@ 'DMARC Record', 'Valid DMARC', 'DMARC Results', 'DMARC Record on Base Domain', 'Valid DMARC Record on Base Domain', 'DMARC Results on Base Domain', 'DMARC Policy', - 'Syntax Errors', 'Debug' + 'Syntax Errors', 'Debug Info' ] # A cache for SMTP scanning results @@ -408,11 +408,11 @@ def scan(domain_name, timeout, smtp_timeout, smtp_localhost, smtp_ports, smtp_ca def handle_error(prefix, domain, error, syntax_error=False): """Handle an error by logging via the Python logging library and - recording it in the debug or syntax_error members of the + recording it in the debug_info or syntax_error members of the trustymail.Domain object. - Since the "Debug" and "Syntax Error" fields in the CSV output of - trustymail come directly from the debug and syntax_error members + Since the "Debug Info" and "Syntax Error" fields in the CSV output of + trustymail come directly from the debug_info and syntax_error members of the trustymail.Domain object, and that CSV is likely all we will have to reconstruct how trustymail reached the conclusions it did, it is vital to record as much helpful information as @@ -458,7 +458,7 @@ def handle_error(prefix, domain, error, syntax_error=False): if syntax_error: domain.syntax_errors.append(error_string) else: - domain.debug.append(error_string) + domain.debug_info.append(error_string) logging.debug(error_string) From 56abb03c4740d9fdc07cb66db648d21a8aa1cb85 Mon Sep 17 00:00:00 2001 From: Shane Frasier Date: Sat, 2 Dec 2017 14:46:27 -0500 Subject: [PATCH 13/14] Cosmetic change --- trustymail/trustymail.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/trustymail/trustymail.py b/trustymail/trustymail.py index a447d7c..bc4fb7e 100755 --- a/trustymail/trustymail.py +++ b/trustymail/trustymail.py @@ -411,12 +411,12 @@ def handle_error(prefix, domain, error, syntax_error=False): recording it in the debug_info or syntax_error members of the trustymail.Domain object. - Since the "Debug Info" and "Syntax Error" fields in the CSV output of - trustymail come directly from the debug_info and syntax_error members - of the trustymail.Domain object, and that CSV is likely all we - will have to reconstruct how trustymail reached the conclusions it - did, it is vital to record as much helpful information as - possible. + Since the "Debug Info" and "Syntax Error" fields in the CSV output + of trustymail come directly from the debug_info and syntax_error + members of the trustymail.Domain object, and that CSV is likely + all we will have to reconstruct how trustymail reached the + conclusions it did, it is vital to record as much helpful + information as possible. Parameters ---------- From 7f50db1bfe987e5a0045fa8e43ac65301891ab74 Mon Sep 17 00:00:00 2001 From: Shane Frasier Date: Mon, 4 Dec 2017 08:32:28 -0500 Subject: [PATCH 14/14] Cosmetic change to docstring --- trustymail/trustymail.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/trustymail/trustymail.py b/trustymail/trustymail.py index bc4fb7e..5243c66 100755 --- a/trustymail/trustymail.py +++ b/trustymail/trustymail.py @@ -463,9 +463,7 @@ def handle_error(prefix, domain, error, syntax_error=False): def handle_syntax_error(prefix, domain, error): - """Convenience method for handle_error(prefix, domain, error, - syntax_error=True) - """ + """Convenience method for handle_error""" handle_error(prefix, domain, error, syntax_error=True)