Skip to content

Commit

Permalink
Merge pull request #110 from echudow/develop
Browse files Browse the repository at this point in the history
Add DNSSEC checks
  • Loading branch information
jsf9k committed Mar 20, 2019
2 parents ce0e05d + 3f27994 commit aede1eb
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 36 deletions.
32 changes: 24 additions & 8 deletions README.md
Expand Up @@ -3,6 +3,8 @@
[![Latest Version](https://img.shields.io/pypi/v/trustymail.svg)](https://pypi.org/project/trustymail/)
[![Build Status](https://travis-ci.com/cisagov/trustymail.svg?branch=develop)](https://travis-ci.com/cisagov/trustymail)
[![Coverage Status](https://coveralls.io/repos/github/cisagov/trustymail/badge.svg?branch=develop)](https://coveralls.io/github/cisagov/trustymail?branch=develop)
[![Total alerts](https://img.shields.io/lgtm/alerts/g/cisagov/trustymail.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/cisagov/trustymail/alerts/)
[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/cisagov/trustymail.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/cisagov/trustymail/context:python)

`trustymail` is a tool that evaluates SPF/DMARC records set in a
domain's DNS. It also checks the mail servers listed in a domain's MX
Expand Down Expand Up @@ -126,6 +128,8 @@ The following values are returned in `results.csv`:
* `MX Record` - If an MX record was found that contains at least a
single mail server.
* `MX Record DNSSEC` - A boolean value indicating whether or not the
DNS record is protected by DNSSEC.
* `Mail Servers` - The list of hosts found in the MX record.
* `Mail Server Ports Tested` - A list of the ports tested for SMTP and
STARTTLS support.
Expand All @@ -141,6 +145,8 @@ The following values are returned in `results.csv`:
### SPF ###
* `SPF Record` - Whether or not a SPF record was found.
* `SPF Record DNSSEC` - A boolean value indicating whether or not the
DNS record is protected by DNSSEC.
* `Valid SPF` - Whether the SPF record found is syntactically correct,
per RFC 4408.
* `SPF Results` - The textual representation of any SPF record found
Expand All @@ -149,20 +155,26 @@ The following values are returned in `results.csv`:
### DMARC ###
* `DMARC Record` - True/False whether or not a DMARC record was found.
* `DMARC Record DNSSEC` - A boolean value indicating whether or not
the DNS record is protected by DNSSEC.
* `Valid DMARC` - Whether the DMARC record found is syntactically
correct.
* `DMARC Results` - The DMARC record that was discovered when querying
DNS.
* `DMARC Record on Base Domain`, `Valid DMARC Record on Base Domain`,
`DMARC Results on Base Domain` - Same definition as above, but
returns the result for the Base Domain. This is important in DMARC
because if there isn't a DMARC record at the domain, the base domain
(or "Organizational Domain", per [RFC
* `DMARC Record on Base Domain`, `DMARC Record on Base Domain DNSSEC`,
`Valid DMARC Record on Base Domain`, `DMARC Results on Base
Domain` - Same definition as above, but returns the result for the
Base Domain. This is important in DMARC because if there isn't a
DMARC record at the domain, the base domain (or "Organizational
Domain", per [RFC
7489](https://tools.ietf.org/html/rfc7489#section-6.6.3)), is
checked and applied.
* `DMARC Policy` - An adjudication, based on any policies found in
`DMARC Results` and `DMARC Results on Base Domain`, of the relevant
DMARC policy that applies.
* `DMARC Subdomain Policy` - An adjudication, based on any policies
found in `DMARC Results` and `DMARC Results on Base Domain`, of the
relevant DMARC subdomain policy that applies.
* `DMARC Policy Percentage` - The percentage of mail that should be
subjected to the `DMARC Policy` according to the `DMARC Results`.
* `DMARC Aggregate Report URIs` - A list of the DMARC aggregate report
Expand All @@ -174,14 +186,18 @@ The following values are returned in `results.csv`:
send DMARC aggregate reports.
* `DMARC Has Forensic Report URI` - A boolean value that indicates if
`DMARC Results` included `ruf` URIs that tell recipients where to
send DMARC forensic reports .
send DMARC forensic reports.
* `DMARC Reporting Address Acceptance Error` - A boolean value that is
True if one or more of the domains listed in the aggregate and
forensic report URIs does not indicate that it accepts DMARC reports
from the domain being tested.
### Etc. ###
* `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
* `Debug Info` - 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.
Expand Down
2 changes: 1 addition & 1 deletion trustymail/__init__.py
@@ -1,6 +1,6 @@
from __future__ import unicode_literals, absolute_import, print_function

__version__ = '0.6.9'
__version__ = '0.7.0'

PublicSuffixListFilename = 'public_suffix_list.dat'
PublicSuffixListReadOnly = False
72 changes: 55 additions & 17 deletions trustymail/domain.py
Expand Up @@ -78,24 +78,28 @@ def __init__(self, domain_name, timeout, smtp_timeout, smtp_localhost, smtp_port
self.is_live = True

# Keep entire record for potential future use.
self.mx_records = []
self.spf = []
self.dmarc = []
self.mx_records = None
self.mx_records_dnssec = None
self.spf = None
self.spf_dnssec = None
self.dmarc = None
self.dmarc_dnssec = False
self.dmarc_policy = None
self.dmarc_subdomain_policy = None
self.dmarc_pct = None
self.dmarc_aggregate_uris = []
self.dmarc_forensic_uris = []
self.dmarc_has_aggregate_uri = False
self.dmarc_has_forensic_uri = False
self.dmarc_reports_address_error = False

# Syntax validity - default spf to false as the lack of an SPF is a bad thing.
self.valid_spf = False
self.valid_dmarc = True
self.syntax_errors = []

# Mail Info
self.mail_servers = []
self.mail_servers = None

# A dictionary for each port for each entry in mail_servers.
# The dictionary's values indicate:
Expand All @@ -111,34 +115,50 @@ def __init__(self, domain_name, timeout, smtp_timeout, smtp_localhost, smtp_port
self.ports_tested = set()

def has_mail(self):
return len(self.mail_servers) > 0
if self.mail_servers is not None:
return len(self.mail_servers) > 0
return None

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'],
self.starttls_results.keys())) > 0
result = None
if len(self.starttls_results) > 0:
result = len(filter(lambda x: self.starttls_results[x]['supports_smtp'],
self.starttls_results.keys())) > 0
return result

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'],
self.starttls_results.keys())) > 0
result = None
if len(self.starttls_results) > 0:
result = len(filter(lambda x: self.starttls_results[x]['starttls'],
self.starttls_results.keys())) > 0
return result

def has_spf(self):
return len(self.spf) > 0
if self.spf is not None:
return len(self.spf) > 0
return None

def has_dmarc(self):
return len(self.dmarc) > 0
if self.dmarc is not None:
return len(self.dmarc) > 0
return None

def add_mx_record(self, record):
if self.mx_records is None:
self.mx_records = []
self.mx_records.append(record)
# The rstrip is because dnspython's string representation of
# the record will contain a trailing period if it is a FQDN.
if self.mail_servers is None:
self.mail_servers = []
self.mail_servers.append(record.exchange.to_text().rstrip('.').lower())

def parent_has_dmarc(self):
Expand All @@ -147,6 +167,12 @@ def parent_has_dmarc(self):
ans = self.base_domain.has_dmarc()
return ans

def parent_dmarc_dnssec(self):
ans = self.dmarc_dnssec
if self.base_domain:
ans = self.base_domain.dmarc_dnssec
return ans

def parent_valid_dmarc(self):
ans = self.valid_dmarc
if self.base_domain:
Expand Down Expand Up @@ -223,18 +249,26 @@ def get_dmarc_forensic_uris(self):
return ans

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']]
domain_supports_smtp = bool(mail_servers_that_support_smtp)
if len(self.starttls_results.keys()) == 0:
domain_supports_smtp = None
domain_supports_starttls = None
mail_servers_that_support_smtp = None
mail_servers_that_support_starttls = None
else:
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_smtp)
domain_supports_starttls = domain_supports_smtp and all([self.starttls_results[x]['starttls'] for x in mail_servers_that_support_smtp])

results = OrderedDict([
('Domain', self.domain_name),
('Base Domain', self.base_domain_name),
('Live', self.is_live),

('MX Record', self.has_mail()),
('MX Record DNSSEC', self.mx_records_dnssec),
('Mail Servers', format_list(self.mail_servers)),
('Mail Server Ports Tested', format_list([str(port) for port in self.ports_tested])),
('Domain Supports SMTP Results', format_list(mail_servers_that_support_smtp)),
Expand All @@ -243,17 +277,20 @@ def generate_results(self):
('Domain Supports STARTTLS Results', 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_starttls),

('SPF Record', self.has_spf()),
('SPF Record DNSSEC', self.spf_dnssec),
('Valid SPF', self.valid_spf),
('SPF Results', format_list(self.spf)),

('DMARC Record', self.has_dmarc()),
('DMARC Record DNSSEC', self.dmarc_dnssec),
('Valid DMARC', self.has_dmarc() and self.valid_dmarc),
('DMARC Results', format_list(self.dmarc)),

('DMARC Record on Base Domain', self.parent_has_dmarc()),
('DMARC Record on Base Domain DNSSEC', self.parent_dmarc_dnssec()),
('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()),
Expand All @@ -265,6 +302,7 @@ def generate_results(self):

('DMARC Has Aggregate Report URI', self.get_dmarc_has_aggregate_uri()),
('DMARC Has Forensic Report URI', self.get_dmarc_has_forensic_uri()),
('DMARC Reporting Address Acceptance Error', self.dmarc_reports_address_error),

('Syntax Errors', format_list(self.syntax_errors)),
('Debug Info', format_list(self.debug_info))
Expand Down

0 comments on commit aede1eb

Please sign in to comment.