├── requirements.txt ├── README.md └── emailseccheck.py /requirements.txt: -------------------------------------------------------------------------------- 1 | colorama==0.4.4 2 | validators==0.18.2 3 | checkdmarc==4.4.1 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is EmailSecCheck? 2 | EmailSecCheck is a lightweight Python utility that checks whether email security DNS records (DMARC and SPF) are configured properly for a domain. EmailSecCheck is powered by [checkdmarc](https://github.com/domainaware/checkdmarc), and leverages it to identify common misconfigurations in DNS records that may enable for email spoofing. 3 | 4 | Email spoofing is identified under the following conditions: 5 | 6 | - SPF Issues 7 | - SPF configured as something other than `fail` or `softfail` 8 | - SPF record is missing 9 | - SPF record contains a syntax error 10 | - DMARC Issues 11 | - Multiple SPF records exist 12 | - DMARC record is missing 13 | - DMARC record contains a syntax error 14 | - Multiple DMARC records exist 15 | 16 | 17 | # Getting Started 18 | Grab the latest release and install the package requirements by running `pip3 install -r requirements.txt`. EmailSecCheck was developed for Python 3. 19 | 20 | ## Checking DNS Records for a Single Domain 21 | ``` 22 | python3 emailseccheck.py --domain 23 | ``` 24 | 25 | ## Checking DNS Records for Several Domains 26 | ``` 27 | python3 emailseccheck.py --domains_file 28 | ``` 29 | 30 | ## Example 31 | ![image](https://user-images.githubusercontent.com/8473031/138940399-452c0f6c-3a4d-4b0a-b5dc-f43d7e6245d3.png) 32 | 33 | -------------------------------------------------------------------------------- /emailseccheck.py: -------------------------------------------------------------------------------- 1 | import validators.domain as validate_domain 2 | import checkdmarc 3 | from colorama import Fore 4 | import os 5 | import sys 6 | import argparse 7 | 8 | 9 | def initialize(): 10 | parser = argparse.ArgumentParser( 11 | prog="emailseccheck.py", 12 | formatter_class=argparse.ArgumentDefaultsHelpFormatter 13 | ) 14 | 15 | domain_argument_group = parser.add_mutually_exclusive_group(required=True) 16 | domain_argument_group.add_argument("--domain", type=str, 17 | help="Domain to check for SPF/DMARC issues") 18 | domain_argument_group.add_argument("--domains_file", type=str, 19 | help="File containing list of domains to check for SPF/DMARC issues") 20 | parser.add_argument("--json", type=str, 21 | help="Output results in JSON format") 22 | args = parser.parse_args() 23 | main(args) 24 | 25 | 26 | def main(args): 27 | validate_args(args) 28 | 29 | domains_list = [] 30 | 31 | if args.domain: 32 | domains_list.append(args.domain) 33 | else: 34 | with open(args.domains_file, "r") as domains_file: 35 | domains_file_content = domains_file.readlines() 36 | domains_list.extend(domains_file_content) 37 | 38 | domains_list = cleanup_domains_list(domains_list) 39 | 40 | if len(domains_list) > 0: 41 | results = check_domain_security(domains_list) 42 | 43 | if args.json: 44 | import json 45 | output = json.dumps(results, indent=2) 46 | 47 | with open(args.json, 'w') as f: 48 | f.write(output) 49 | 50 | else: 51 | print_error("No domain(s) were provided") 52 | 53 | 54 | def cleanup_domains_list(domains_list): 55 | domains_list = [d.lower() for d in domains_list] 56 | domains_list = list(dict.fromkeys(domains_list)) 57 | 58 | domains_list.sort() 59 | return domains_list 60 | 61 | 62 | def validate_args(args): 63 | domain_arg_valid = args.domain is None or validate_domain(args.domain) 64 | domain_file_arg_valid = args.domains_file is None or os.path.isfile( 65 | args.domains_file) 66 | 67 | if not domain_arg_valid: 68 | print_warning("Domain is not valid. Is it formatted correctly?") 69 | elif not domain_file_arg_valid: 70 | print_warning("Domain file is not valid. Does it exist?") 71 | 72 | valid_args = domain_arg_valid and domain_file_arg_valid 73 | if not valid_args: 74 | print_error("Arguments are invalid.") 75 | sys.exit(1) 76 | 77 | return valid_args 78 | 79 | 80 | def validate_provided_domains(domains): 81 | for domain in domains: 82 | if not validate_domain(domain): 83 | print_error("Invalid domain provided (%s)" % domain) 84 | sys.exit(1) 85 | 86 | 87 | def check_domain_security(domains): 88 | print_info("Analyzing %d domain(s)..." % len(domains)) 89 | 90 | spoofable_domains = [] 91 | results = { 92 | "domains": {}, 93 | "spoofable_domains": [] 94 | } 95 | 96 | for domain in domains: 97 | domain = domain.strip() 98 | print_info("Analyzing %s" % domain) 99 | 100 | domain_result = { 101 | 102 | "spf_issues": [], 103 | "dmarc_issues": [], 104 | "spoofable": False 105 | } 106 | 107 | spoofing_possible_spf = False 108 | spoofing_possible_dmarc = False 109 | 110 | try: 111 | spf_results = checkdmarc.get_spf_record(domain) 112 | 113 | spf_value = spf_results["parsed"]["all"] 114 | 115 | if spf_value != 'fail': 116 | spoofing_possible_spf = True 117 | if spf_value == "softfail": 118 | print_warning( 119 | "SPF record configured to 'softfail' for '%s'" % domain) 120 | domain_result["spf_issues"].append("SPF record configured to 'softfail'") 121 | else: 122 | print_warning( 123 | "SPF record missing failure behavior value for '%s'" % domain) 124 | domain_result["spf_issues"].append("SPF record missing failure behavior value") 125 | except checkdmarc.DNSException: 126 | print_error( 127 | "A general DNS error has occured when performing SPF analysis") 128 | domain_result["spf_issues"].append("DNS error during SPF analysis") 129 | except checkdmarc.SPFIncludeLoop: 130 | print_warning( 131 | "SPF record contains an 'include' loop for '%s'" % domain) 132 | domain_result["spf_issues"].append("SPF record contains an 'include' loop") 133 | except checkdmarc.SPFRecordNotFound: 134 | print_warning("SPF record is missing for '%s'" % domain) 135 | domain_result["spf_issues"].append("SPF record is missing") 136 | spoofing_possible_spf = True 137 | except checkdmarc.SPFRedirectLoop: 138 | print_warning("SPF record contains a 'redirect' loop for '%s'" % domain) 139 | domain_result["spf_issues"].append("SPF record contains a 'redirect' loop") 140 | except checkdmarc.SPFSyntaxError: 141 | print_warning("SPF record contains a syntax error for '%s'" % domain) 142 | domain_result["spf_issues"].append("SPF record contains a syntax error") 143 | spoofing_possible_spf = True 144 | except checkdmarc.SPFTooManyDNSLookups: 145 | print_warning("SPF record requires too many DNS lookups for '%s'" % domain) 146 | domain_result["spf_issues"].append("SPF record requires too many DNS lookups") 147 | except checkdmarc.MultipleSPFRTXTRecords: 148 | print_warning("Multiple SPF records were found for '%s'" % domain) 149 | domain_result["spf_issues"].append("Multiple SPF records found") 150 | spoofing_possible_spf = True 151 | 152 | try: 153 | dmarc_data = checkdmarc.get_dmarc_record(domain) 154 | except checkdmarc.DNSException: 155 | print_error("A general DNS error has occured when performing DMARC analysis") 156 | domain_result["dmarc_issues"].append("DNS error during DMARC analysis") 157 | except checkdmarc.DMARCRecordInWrongLocation: 158 | print_warning("DMARC record is located in the wrong domain for '%s'" % domain) 159 | domain_result["dmarc_issues"].append("DMARC record is in wrong location") 160 | except checkdmarc.DMARCRecordNotFound: 161 | print_warning("DMARC record is missing for '%s'" % domain) 162 | domain_result["dmarc_issues"].append("DMARC record is missing") 163 | spoofing_possible_dmarc = True 164 | except checkdmarc.DMARCReportEmailAddressMissingMXRecords: 165 | print_warning("DMARC record's report URI contains a domain with invalid MX records for '%s'" % domain) 166 | domain_result["dmarc_issues"].append("DMARC report URI has invalid MX records") 167 | except checkdmarc.DMARCSyntaxError: 168 | print_warning("DMARC record contains a syntax error for '%s'" % domain) 169 | domain_result["dmarc_issues"].append("DMARC record contains a syntax error") 170 | spoofing_possible_dmarc = True 171 | except checkdmarc.InvalidDMARCReportURI: 172 | print_warning("DMARC record references an invalid report URI for '%s'" % domain) 173 | domain_result["dmarc_issues"].append("DMARC record has invalid report URI") 174 | except checkdmarc.InvalidDMARCTag: 175 | print_warning("DMARC record contains an invalid tag for '%s'" % domain) 176 | domain_result["dmarc_issues"].append("DMARC record contains invalid tag") 177 | except checkdmarc.MultipleDMARCRecords: 178 | print_warning("Multiple DMARC records were found for '%s'" % domain) 179 | domain_result["dmarc_issues"].append("Multiple DMARC records found") 180 | spoofing_possible_dmarc = True 181 | 182 | if spoofing_possible_spf or spoofing_possible_dmarc: 183 | spoofable_domains.append(domain) 184 | domain_result["spoofable"] = True 185 | results["spoofable_domains"].append(domain) 186 | 187 | results["domains"][domain] = domain_result 188 | 189 | if len(spoofable_domains) > 0: 190 | print(Fore.CYAN, "\n\n Spoofing possible for %d domain(s): " % len(spoofable_domains)) 191 | for domain in spoofable_domains: 192 | print(Fore.CYAN, " > %s" % domain) 193 | else: 194 | print(Fore.GREEN, "\n\n No spoofable domains were identified") 195 | 196 | return results 197 | 198 | 199 | def print_error(message, fatal=True): 200 | print(Fore.RED, "[!] ERROR: %s" % message) 201 | if fatal: 202 | sys.exit(1) 203 | 204 | 205 | def print_warning(message): 206 | print(Fore.YELLOW, "[-] WARN: %s" % message) 207 | 208 | 209 | def print_info(message): 210 | print(Fore.LIGHTBLUE_EX, "[+] INFO: %s" % message) 211 | 212 | 213 | if __name__ == "__main__": 214 | initialize() 215 | --------------------------------------------------------------------------------