domainaware / checkdmarc

A parser for SPF and DMARC DNS records
https://domainaware.github.io/checkdmarc
Apache License 2.0
251 stars 77 forks source link

DMARC Fatal exception when external domain verification fails to resolve. #54

Closed cortesce closed 3 years ago

cortesce commented 4 years ago

Hi, when external domain doesn't resolve in NS, raise a fatal DMARC exception, and looses all the DMARC informatión obtained from domain.

Example Domain: vodafonefastforward.es

Run example: checkdmarc.py vodafonefastforward.es result = checkdmarc.get_dmarc_record('vodafonefastforward.es',include_tag_descriptions=True)

Show all information about NS, MX, ... but about DMARC, only a warning message.

gyro.com does not indicate that it accepts DMARC reports about vodafonefastforward.es - Authorization record not found: vodafonefastforward.es._report._dmarc.gyro.com IN TXT "v=DMARC1"

And not return the information obtained previously in the

v=DMARC1; p=quarantine; pct=5; rua=mailto:barto.herron@gyro.com

I don't know if is a bug. I'm working on it, for don't loose the tags (dict) obtained, even though no external domain validation confirmation.

def verify_dmarc_report_destination(source_domain, destination_domain, nameservers=None, timeout=2.0): """ Checks if the report destination accepts reports for the source domain per RFC 7489, section 7.1

  Args:
      source_domain (str): The source domain
      destination_domain (str): The destination domain
      nameservers (list): A list of nameservers to query
      (Cloudflare's by default)
      timeout(float): number of seconds to wait for an answer from DNS

  Returns:
      bool: Indicates if the report domain accepts reports from the given
      domain

  Raises:
      :exc:`checkdmarc.UnverifiedDMARCURIDestination`
      :exc:`checkdmarc.UnrelatedTXTRecordFound`
  """

source_domain = source_domain.lower()
destination_domain = destination_domain.lower()

if get_base_domain(source_domain) != get_base_domain(destination_domain):
    if check_wildcard_dmarc_report_authorization(destination_domain,
                                                 nameservers=nameservers):
        return True
    target = "{0}._report._dmarc.{1}".format(source_domain,
                                             destination_domain)
    message = "{0} does not indicate that it accepts DMARC reports " \
              "about {1} - " \
              "Authorization record not found: " \
              '{2} IN TXT "v=DMARC1"'.format(destination_domain,
                                             source_domain,
                                             target)
    dmarc_record_count = 0
    unrelated_records = []
    try:
        records = _query_dns(target, "TXT",
                             nameservers=nameservers,
                             timeout=timeout)

        for record in records:
            if record.startswith("v=DMARC1"):
                dmarc_record_count += 1
            else:
                unrelated_records.append(record)

        if len(unrelated_records) > 0:
            raise UnrelatedTXTRecordFoundAtDMARC(
                "Unrelated TXT records were discovered. "
                "These should be removed, as some "
                "receivers may not expect to find unrelated TXT records "
                "at {0}\n\n{1}".format(target,
                                       "\n\n".join(unrelated_records)))

        if dmarc_record_count < 1:
            raise UnverifiedDMARCURIDestination(message)
    except Exception:
        raise UnverifiedDMARCURIDestination(message)

return True

Thanks in advance.

SimonGurney commented 3 years ago

I would have to agree that I do not like this logic. Currently, should one rua or ruf fail validation, the DMARC is not parsed at all. This seems counter-intuitive as a single failed rua/ruf is not necessarily an issue. Should this not be an appended warning as long as at least one valid rua/rufis specified? Even better, a warning and as another attr, maybe "failed_rua" and "failed_ruf" so someone wanting this information can run standard logic tests against those attributes?

I am using this module to parse DMARC records so a large bulk of domains can be analysed / visualised and so I have had to monkey patch the verification out, otherwise I cannot get a partially parsed record which would be useful to at least see the policy ("reject" etc).

Monkey Patch to remove some of the 'this record isn't quite right' logic

import ast
import inspect
s = inspect.getsource(checkdmarc.parse_dmarc_record)
m = ast.parse(s)
del m.body[0].body[18].body[0].body[2].body[7]  # Verify dest  
del m.body[0].body[18].body[0].body[2].body[5]  # Verify mx  
del m.body[0].body[18].body[0].body[2].body[4]  # Verify not more than 2  
del m.body[0].body[19].body[0].body[2].body[7]  # Verify dest  
del m.body[0].body[19].body[0].body[2].body[5]  # Verify mx  
del m.body[0].body[19].body[0].body[2].body[4]  # Verify not more than 2  
co = compile(m, "<string>", "exec")
exec(co, checkdmarc.__dict__)