15
0

- Update to 5.3.1:

* Ignore UnicodeDecodeError exceptions when querying for TXT records
  * Check DNSSEC on MX hostnames
  * USE DNSSEC when requesting DNSKEY records
  * Do not require an RRSIG answer when querying for DNSKEY records
  * Pass in nameservers and timeout when running get_dnskey recursively
  * Properly cache DNSKEY answers
  * Fix exception handling for query_mta_sts_record
  * Check for TLSA records
  * Add support for parsing SMTP TLS Reporting (RFC8460) DNS records
  * Add missing import dns.dnssec
  * Always use the actual subdomain or domain provided
  * Include MTA-STS and BIMI results in CSV output
  * Added the include_tag_descriptions parameter to
    checkdmarc.bimi.check_bimi()
  * Added the exception class MTASTSPolicyDownloadError
  * Major refactoring: Change from a single module to a package of modules,
    with each checked standard as its own package
  * Add support for MTA-STS RFC 8461
  * Add support for BIMI
  * Specify a BIMI selector using the --bimi-selector/-b option
  * Fix SPF query error and warning messages
  * Add support for null MX records - RFC 7505
  * Make DMARC retorting URI error messages more clear
  * Fix compatibility with Python 3.8
  * SPFRecordNotFound exception now includes a domain argument
  * The DMARC missing authorization error message now includes the full
    expected DNS record
  * Properly parse DMARC and BIMI records for domains that do not have an
    identified base domain

OBS-URL: https://build.opensuse.org/package/show/devel:languages:python/python-checkdmarc?expand=0&rev=5
This commit is contained in:
2024-02-29 01:48:13 +00:00
committed by Git OBS Bridge
parent ad99ac0b70
commit 7f1ac1cbd4
8 changed files with 255 additions and 355 deletions

157
tests.py
View File

@@ -1,6 +1,16 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Automated tests"""
import unittest
from collections import OrderedDict
import checkdmarc
import checkdmarc.utils
import checkdmarc.spf
import checkdmarc.dmarc
import checkdmarc.dnssec
known_good_domains = [
"fbi.gov",
@@ -34,12 +44,53 @@ class Test(unittest.TestCase):
"Known good domain {0} failed DMARC check:"
"\n\n{1}".format(result["domain"], dmarc_error))
def testDMARCMixedFormatting(self):
"""DMARC records with extra spaces and mixed case are still valid"""
examples = [
"v=DMARC1;p=ReJect",
"v = DMARC1;p=reject;",
"v = DMARC1\t;\tp=reject\t;",
"v = DMARC1\t;\tp\t\t\t=\t\t\treject\t;",
"V=DMARC1;p=reject;"
]
for example in examples:
parsed_record = checkdmarc.dmarc.parse_dmarc_record(example, "")
self.assertIsInstance(parsed_record, OrderedDict)
def testGetBaseDomain(self):
subdomain = "foo.example.com"
result = checkdmarc.utils.get_base_domain(subdomain)
assert result == "example.com"
# Test reserved domains
subdomain = "_dmarc.nonauth-rua.invalid.example"
result = checkdmarc.utils.get_base_domain(subdomain)
assert result == "invalid.example"
subdomain = "_dmarc.nonauth-rua.invalid.test"
result = checkdmarc.utils.get_base_domain(subdomain)
assert result == "invalid.test"
subdomain = "_dmarc.nonauth-rua.invalid.invalid"
result = checkdmarc.utils.get_base_domain(subdomain)
assert result == "invalid.invalid"
subdomain = "_dmarc.nonauth-rua.invalid.localhost"
result = checkdmarc.utils.get_base_domain(subdomain)
assert result == "invalid.localhost"
# Test newer PSL entries
subdomain = "e3191.c.akamaiedge.net"
result = checkdmarc.utils.get_base_domain(subdomain)
assert result == "c.akamaiedge.net"
def testUppercaseSPFMechanism(self):
"""Treat uppercase SPF"SPF mechanisms as valid"""
spf_record = "v=spf1 IP4:147.75.8.208 -ALL"
domain = "example.no"
results = checkdmarc.parse_spf_record(spf_record, domain)
results = checkdmarc.spf.parse_spf_record(spf_record, domain)
self.assertEqual(len(results["warnings"]), 0)
@@ -48,7 +99,7 @@ class Test(unittest.TestCase):
rec = '"v=spf1 ip4:147.75.8.208 " "include:_spf.salesforce.com -all"'
parsed_record = checkdmarc.parse_spf_record(rec, "example.com")
parsed_record = checkdmarc.spf.parse_spf_record(rec, "example.com")
self.assertEqual(parsed_record["parsed"]["all"], "fail")
@@ -57,12 +108,13 @@ class Test(unittest.TestCase):
rec = "v=spf1 ip4:213.5.39.110 -all MS=83859DAEBD1978F9A7A67D3"
domain = "avd.dk"
parsed_record = checkdmarc.parse_spf_record(rec, domain)
parsed_record = checkdmarc.spf.parse_spf_record(rec, domain)
self.assertEqual(len(parsed_record["warnings"]), 1)
@unittest.skip
def testDNSSEC(self):
"""Test known good DNSSEC"""
self.assertEqual(checkdmarc.test_dnssec("whalensolutions.com"), True)
self.assertEqual(checkdmarc.dnssec.test_dnssec("fbi.gov"), True)
def testIncludeMissingSPF(self):
"""SPF records that include domains that are missing SPF records
@@ -74,8 +126,8 @@ class Test(unittest.TestCase):
'include:mail2.dialogportal.com a:mailrelay.jppol.dk ' \
'a:sendmail.jppol.dk ?all"'
domain = "ekstrabladet.dk"
self.assertRaises(checkdmarc.SPFRecordNotFound,
checkdmarc.parse_spf_record, spf_record, domain)
self.assertRaises(checkdmarc.spf.SPFRecordNotFound,
checkdmarc.spf.parse_spf_record, spf_record, domain)
def testTooManySPFDNSLookups(self):
"""SPF records with > 10 SPF mechanisms that cause DNS lookups raise
@@ -89,8 +141,19 @@ class Test(unittest.TestCase):
"include:_spf.google.com " \
"~all"
domain = "example.com"
self.assertRaises(checkdmarc.SPFTooManyDNSLookups,
checkdmarc.parse_spf_record, spf_record, domain)
self.assertRaises(checkdmarc.spf.SPFTooManyDNSLookups,
checkdmarc.spf.parse_spf_record, spf_record, domain)
def testTooManySPFVoidDNSLookups(self):
"""SPF records with > 2 void DNS lookups"""
spf_record = "v=spf1 a:13Mk4olS9VWhQqXRl90fKJrD.example.com " \
"mx:SfGiqBnQfRbOMapQJhozxo2B.example.com " \
"a:VAFeyU9N2KJX518aGsN3w6VS.example.com " \
"~all"
domain = "example.com"
self.assertRaises(checkdmarc.spf.SPFTooManyVoidDNSLookups,
checkdmarc.spf.parse_spf_record, spf_record, domain)
def testSPFSyntaxErrors(self):
"""SPF record syntax errors raise SPFSyntaxError"""
@@ -98,8 +161,8 @@ class Test(unittest.TestCase):
spf_record = '"v=spf1 mx a:mail.cohaesio.net ' \
'include: trustpilotservice.com ~all"'
domain = "2021.ai"
self.assertRaises(checkdmarc.SPFSyntaxError,
checkdmarc.parse_spf_record, spf_record, domain)
self.assertRaises(checkdmarc.spf.SPFSyntaxError,
checkdmarc.spf.parse_spf_record, spf_record, domain)
def testSPFInvalidIPv4(self):
"""Invalid ipv4 SPF mechanism values raise SPFSyntaxError"""
@@ -107,56 +170,70 @@ class Test(unittest.TestCase):
"+ip4:78.46.224.83 " \
"+ip4:relay.mailchannels.net +ip4:138.201.60.20 ~all"
domain = "surftown.dk"
self.assertRaises(checkdmarc.SPFSyntaxError,
checkdmarc.parse_spf_record, spf_record, domain)
self.assertRaises(checkdmarc.spf.SPFSyntaxError,
checkdmarc.spf.parse_spf_record, spf_record, domain)
def testSPFInvalidIPv6inIPv4(self):
"""Invalid ipv4 SPF mechanism values raise SPFSyntaxError"""
spf_record = "v=spf1 ip4:1200:0000:AB00:1234:0000:2552:7777:1313 ~all"
domain = "surftown.dk"
self.assertRaises(checkdmarc.spf.SPFSyntaxError,
checkdmarc.spf.parse_spf_record, spf_record, domain)
def testSPFInvalidIPv4Range(self):
"""Invalid ipv4 SPF mechanism values raise SPFSyntaxError"""
spf_record = "v=spf1 ip4:78.46.96.236/99 ~all"
domain = "surftown.dk"
self.assertRaises(checkdmarc.SPFSyntaxError,
checkdmarc.parse_spf_record, spf_record, domain)
self.assertRaises(checkdmarc.spf.SPFSyntaxError,
checkdmarc.spf.parse_spf_record, spf_record, domain)
def testSPFInvalidIPv6(self):
"""Invalid ipv6 SPF mechanism values raise SPFSyntaxError"""
spf_record = "v=spf1 ip6:1200:0000:AB00:1234:O000:2552:7777:1313 ~all"
domain = "surftown.dk"
self.assertRaises(checkdmarc.SPFSyntaxError,
checkdmarc.parse_spf_record, spf_record, domain)
self.assertRaises(checkdmarc.spf.SPFSyntaxError,
checkdmarc.spf.parse_spf_record, spf_record, domain)
def testSPFInvalidIPv4inIPv6(self):
"""Invalid ipv6 SPF mechanism values raise SPFSyntaxError"""
spf_record = "v=spf1 ip6:78.46.96.236 ~all"
domain = "surftown.dk"
self.assertRaises(checkdmarc.spf.SPFSyntaxError,
checkdmarc.spf.parse_spf_record, spf_record, domain)
def testSPFInvalidIPv6Range(self):
"""Invalid ipv6 SPF mechanism values raise SPFSyntaxError"""
record = "v=spf1 ip6:1200:0000:AB00:1234:0000:2552:7777:1313/130 ~all"
domain = "surftown.dk"
self.assertRaises(checkdmarc.SPFSyntaxError,
checkdmarc.parse_spf_record, record, domain)
self.assertRaises(checkdmarc.spf.SPFSyntaxError,
checkdmarc.spf.parse_spf_record, record, domain)
def testSPFIncludeLoop(self):
"""SPF record with include loop raises SPFIncludeLoop"""
spf_record = '"v=spf1 include:example.com"'
domain = "example.com"
self.assertRaises(checkdmarc.SPFIncludeLoop,
checkdmarc.parse_spf_record, spf_record, domain)
self.assertRaises(checkdmarc.spf.SPFIncludeLoop,
checkdmarc.spf.parse_spf_record, spf_record, domain)
def testSPFMissingMXRecord(self):
"""A warning is issued if a SPF record contains a mx mechanism
"""A warning is issued if an SPF record contains a mx mechanism
pointing to a domain that has no MX records"""
spf_record = '"v=spf1 mx ~all"'
domain = "seanthegeek.net"
results = checkdmarc.parse_spf_record(spf_record, domain)
results = checkdmarc.spf.parse_spf_record(spf_record, domain)
self.assertIn("{0} does not have any MX records".format(domain),
results["warnings"])
def testSPFMissingARecord(self):
"""A warning is issued if a SPF record contains a mx mechanism
"""A warning is issued if an SPF record contains a mx mechanism
pointing to a domain that has no A records"""
spf_record = '"v=spf1 include:_spf.bibsyst.no a mx ~all"'
domain = "sogne.folkebibl.no"
results = checkdmarc.parse_spf_record(spf_record, domain)
self.assertIn("sogne.folkebibl.no does not have any A/AAAA records",
spf_record = '"v=spf1 a ~all"'
domain = "cardinalhealth.net"
results = checkdmarc.spf.parse_spf_record(spf_record, domain)
self.assertIn("cardinalhealth.net does not have any A/AAAA records",
results["warnings"])
def testDMARCPctLessThan100Warning(self):
@@ -167,7 +244,7 @@ class Test(unittest.TestCase):
"rua=mailto:eits.dmarcrua@energy.gov; " \
"ruf=mailto:eits.dmarcruf@energy.gov"
domain = "energy.gov"
results = checkdmarc.parse_dmarc_record(dmarc_record, domain)
results = checkdmarc.dmarc.parse_dmarc_record(dmarc_record, domain)
self.assertIn("pct value is less than 100",
results["warnings"][0])
@@ -177,34 +254,26 @@ class Test(unittest.TestCase):
dmarc_record = "v=DMARC1; p=none; rua=reports@dmarc.cyber.dhs.gov," \
"mailto:dmarcreports@usdoj.gov"
domain = "dea.gov"
self.assertRaises(checkdmarc.InvalidDMARCReportURI,
checkdmarc.parse_dmarc_record, dmarc_record, domain)
self.assertRaises(checkdmarc.dmarc.InvalidDMARCReportURI,
checkdmarc.dmarc.parse_dmarc_record, dmarc_record,
domain)
dmarc_record = "v=DMARC1; p=none; rua=__" \
"mailto:reports@dmarc.cyber.dhs.gov," \
"mailto:dmarcreports@usdoj.gov"
self.assertRaises(checkdmarc.InvalidDMARCReportURI,
checkdmarc.parse_dmarc_record, dmarc_record, domain)
self.assertRaises(checkdmarc.dmarc.InvalidDMARCReportURI,
checkdmarc.dmarc.parse_dmarc_record, dmarc_record,
domain)
def testInvalidDMARCPolicyValue(self):
"""An invalid DMARC policy value raises InvalidDMARCTagValue """
dmarc_record = "v=DMARC1; p=foo; rua=mailto:dmarc@example.com"
domain = "example.com"
self.assertRaises(checkdmarc.InvalidDMARCTagValue,
checkdmarc.parse_dmarc_record,
self.assertRaises(checkdmarc.dmarc.InvalidDMARCTagValue,
checkdmarc.dmarc.parse_dmarc_record,
dmarc_record,
domain)
def testInvalidDMARCfo(self):
"""An invalid DMARC fo tag value raises InvalidDMARCTagValue"""
dmarc_record = "v=DMARC1;p=none;aspf=s;adkim=s;fo=0:1:d:s;" \
"ruf=mailto:dmarcreports@omb.gov;" \
"rua=mailto:dmarcreports@omb.gov"
domain = "omb.gov"
self.assertRaises(checkdmarc.InvalidDMARCTagValue,
checkdmarc.parse_dmarc_record, dmarc_record, domain)
if __name__ == "__main__":
unittest.main(verbosity=2)