├── .DS_Store ├── .gitattributes ├── .gitignore ├── .idea └── .idea.Elyzer.dir │ └── .idea │ ├── .gitignore │ ├── encodings.xml │ ├── indexLayout.xml │ └── vcs.xml ├── LICENSE ├── README.md ├── core ├── __init__.py ├── colors.py ├── exp_json.py ├── spoofing.py └── utils.py ├── dist └── elyzer.exe ├── elyzer.py └── requirements.txt /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mochabyte0x/Elyzer/132da05e1830aac57ac7f5f1266c3d725a6c05c2/.DS_Store -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | header* 2 | elyzer.spec 3 | mail.ico 4 | Elyzer_Logo.png 5 | Elyzer_Logo_Black.png 6 | Elyzer_Logo.ico 7 | __pycache__ 8 | .idea 9 | -------------------------------------------------------------------------------- /.idea/.idea.Elyzer.dir/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Rider ignored files 5 | /projectSettingsUpdater.xml 6 | /modules.xml 7 | /contentModel.xml 8 | /.idea.Elyzer.iml 9 | # Editor-based HTTP Client requests 10 | /httpRequests/ 11 | # Datasource local storage ignored files 12 | /dataSources/ 13 | /dataSources.local.xml 14 | -------------------------------------------------------------------------------- /.idea/.idea.Elyzer.dir/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/.idea.Elyzer.dir/.idea/indexLayout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/.idea.Elyzer.dir/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Arthur Minasyan (B0lg0r0v) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elyzer 2 | 3 |

4 | 5 |

6 | 7 | # Table of Contents 8 | 9 | - [Elyzer](#elyzer) 10 | * [Description](#description) 11 | * [General Informations](#general-informations) 12 | * [Installation](#installation) 13 | * [Usage](#usage) 14 | * [Features](#features) 15 | * [To-Do](#to-do) 16 | * [Community Projects](#community-projects) 17 | * [Notes](#notes) 18 | * [Disclaimer](#disclaimer) 19 | 20 | ## Description 21 | 22 | Elyzer is an e-mail header analyzer capable of detecting potential spoofing attempts. It will give you general information about the e-mail, the route it took, important security headers and the phishing / spoofing results.

:warning: *This project is under active development, and changes will be made frequently. As it's still in the early stages, bugs may be present.*.
23 | 24 | ## General Informations 25 | 26 | - Before using this tool, make sure the e-mail header is formated correctly. This tool will parse the header according to RFC 822. 27 | - This tool can ONLY utilize the spoofing / phishing function if the header contains the sender's SMTP Server IPv4 address. IPv6 addresses are currently not supported. 28 | - Microsoft e-mail services are using IPv6 addresses, which on top of that are proxys. Finding the source address is very difficult if not simply impossible. 29 | - PLEASE DO NOT RELY ONLY ON THIS TOOL. Elyzer cannot garantuee you 100% accuracy. 30 | 31 | ## Installation 32 | 33 | **For Unix users:** 34 | ``` 35 | git clone https://github.com/B0lg0r0v/Elyzer.git 36 | cd Elyzer 37 | python -m pip install -r requirements.txt 38 | ``` 39 | 40 | To use the `-pa` argument, you need one API key from Driftnet: 41 | 42 | - Driftnet API Key (https://driftnet.io) 43 | 44 | Create an environment variable called `DRIFTNET_API` and insert your key as a value. 45 | 46 | ``` 47 | # On Unix systems 48 | export DRIFTNET_API= 49 | 50 | # On Windows 51 | set DRIFTNET_API= 52 | ``` 53 | 54 | ## Usage 55 | Using Elyzer is quite intuitive. Give with the *-f* argument the header file. 56 | 57 | **Unix:** 58 | ``` 59 | python3 elyzer.py -f 60 | ``` 61 | 62 | Full Elyzer options: 63 | 64 | ``` 65 | options: 66 | -h, --help show this help message and exit 67 | -f FILE, --file FILE Give the E-Mail Header as a file. 68 | -pa, --passive Enables the passive mode. DNS resolution is performed passively through Driftnet 69 | for better OPSEC. You need to add "DRIFTNET_API" as an environment variable to 70 | use this feature. 71 | -nd, --no-dns Enables the no-dns mode. No DNS resolution is performed for best OPSEC. This heavily affects 72 | the results ! 73 | -q, --quiet Quiet mode. Disables banner. 74 | -j, --json EXPERIMENTAL FEATURE. Output the results in JSON format. 75 | -v, --version show program's version number and exit 76 | -a ATTACHEMENT, --attachement ATTACHEMENT 77 | Check if the file is malicious. 78 | ``` 79 | 80 | Elyzer performs various DNS lookups to compare values for the spoofing function. This could raise OPSEC concerns, especially when dealing with a targeted attack. 81 | 82 | If you have OPSEC concerns, you can now use the `-pa` argument to perform DNS lookups passively. This way, you’re no longer *directly* interacting with potential malicious domains, but *indirectly*, making it harder for an adversary to track. However, this CAN impact the results. 83 | 84 | ``` 85 | python3 elyzer.py -f -pa 86 | ``` 87 | 88 | If you want the best OPSEC, you can use the `-nd` argument, which enables 'No DNS / Paranoid' mode. This will disable all DNS lookups, allowing you to use Elyzer entirely offline. However, be aware that this will significantly impact the results ! 89 | 90 | ``` 91 | python3 elyzer.py -f -nd 92 | ``` 93 | 94 | Additionally you can give a file with the `-a` argument to Elyzer. It will then generate you a VirusTotal Link where you can see if the file is potentially malicious or not. 95 | 96 | ``` 97 | python3 elyzer.py -f -a 98 | ``` 99 | 100 | ## Features 101 | Here's a quick overview of Elyzer's features: 102 | - Print general e-mail informations 103 | - Print relay routing with timestamps 104 | - Print security headers and check if set correctly 105 | - Print interesting headers such as "Envelope-From" 106 | - Print MS-Exchange Headers 107 | - Spoofing / Phishing analyzer with optional passive DNS lookup 108 | 109 | *Spoofing / Phishing detection feature:* 110 |

111 | 112 | 113 | ## To-Do 114 | - [ ] Add JSON output functionality. 115 | - [x] Add a functionality to be able to passively query DNS information to reduce OPSEC concerns. 116 | - [x] Switching entirely to the Driftnet API 117 | - [ ] Optimize my garbage code :D 118 | 119 | ## Community Projects 120 | 121 | Check out this awesome project by [@adriy-be](https://github.com/adriy-be): a WebUI for Elyzer!
Github repo: https://github.com/adriy-be/ElyzerWebUi 122 | 123 | ## Notes 124 | Credits for the *getReceivedFields* & the *getFields* functions goes to "spcnvdr" , Copyright 2020.
125 | Also, thanks to [@triggerfx](https://github.com/triggerfx) for the custom Logo ! 126 | 127 | ## Disclaimer 128 | This tool is primarly created for me as a project to enhance my coding skills and start creating some red team / blue team tools. It is not considered to be the most efficient tool out there. 129 | 130 | -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mochabyte0x/Elyzer/132da05e1830aac57ac7f5f1266c3d725a6c05c2/core/__init__.py -------------------------------------------------------------------------------- /core/colors.py: -------------------------------------------------------------------------------- 1 | import colorama 2 | from colorama import Fore, Style 3 | 4 | colorama.init(autoreset=True) 5 | 6 | class Colors: 7 | @staticmethod 8 | def red(str): 9 | return Fore.RED + str + Style.RESET_ALL 10 | 11 | @staticmethod 12 | def green(str): 13 | return Fore.GREEN + str + Style.RESET_ALL 14 | 15 | @staticmethod 16 | def yellow(str): 17 | return Fore.YELLOW + str + Style.RESET_ALL 18 | 19 | @staticmethod 20 | def blue(str): 21 | return Fore.BLUE + str + Style.RESET_ALL 22 | 23 | @staticmethod 24 | def magenta(str): 25 | return Fore.MAGENTA + str + Style.RESET_ALL 26 | 27 | @staticmethod 28 | def cyan(str): 29 | return Fore.CYAN + str + Style.RESET_ALL 30 | 31 | @staticmethod 32 | def white(str): 33 | return Fore.WHITE + str + Style.RESET_ALL 34 | 35 | @staticmethod 36 | def black(str): 37 | return Fore.BLACK + str + Style.RESET_ALL 38 | 39 | @staticmethod 40 | def light_red(str): 41 | return Fore.LIGHTRED_EX + str + Style.RESET_ALL 42 | 43 | @staticmethod 44 | def light_green(str): 45 | return Fore.LIGHTGREEN_EX + str + Style.RESET_ALL 46 | 47 | @staticmethod 48 | def light_yellow(str): 49 | return Fore.LIGHTYELLOW_EX + str + Style.RESET_ALL 50 | 51 | @staticmethod 52 | def light_blue(str): 53 | return Fore.LIGHTBLUE_EX + str + Style.RESET_ALL 54 | 55 | @staticmethod 56 | def light_magenta(str): 57 | return Fore.LIGHTMAGENTA_EX + str + Style.RESET_ALL 58 | 59 | 60 | def banner(): 61 | print(r""" 62 | 63 | ____ ____ __ ____ ____ ___ 64 | / __// /\ \/ //_ / / __// _ \ 65 | / _/ / /__\ / / /_ / _/ / , _/ 66 | /___//____//_/ /___//___//_/|_| v0.5.0 67 | 68 | """+ 69 | (Colors.light_blue("\n\tAuthor: B0lg0r0v") + Colors.light_blue("\n\thttps://arthurminasyan.com\n"))) 70 | 71 | -------------------------------------------------------------------------------- /core/exp_json.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import json 4 | from core.utils import Utils 5 | from datetime import datetime 6 | from core.spoofing import Spoofing 7 | 8 | def parse_key_value(text): 9 | result = {} 10 | for line in text.split('\n'): 11 | line = line.strip() 12 | if ':' in line: 13 | key, value = line.split(':', 1) 14 | result[key.strip()] = value.strip() 15 | return result 16 | 17 | def export_to_json(results, filename=None): 18 | if filename is None: 19 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") 20 | filename = f"email_analysis_{timestamp}.json" 21 | 22 | # Ensure the directory exists 23 | os.makedirs(os.path.dirname(filename), exist_ok=True) 24 | 25 | with open(filename, 'w', encoding='utf-8') as f: 26 | json.dump(results, f, ensure_ascii=False, indent=4) 27 | 28 | return filename 29 | 30 | class ExportJson: 31 | 32 | def __init__(self, file): 33 | self.utils = Utils(file) 34 | self.spoofing = Spoofing(file) 35 | 36 | 37 | def jsonoutput(self): 38 | return { 39 | "general_information": self.parse_general_information(), 40 | "security_information": self.parse_security_information(), 41 | "routing": self.parse_routing(), 42 | "envelope": self.parse_envelope(), 43 | "spoofing_check": self.parse_spoofing_check(), 44 | } 45 | 46 | 47 | def parse_general_information(self): 48 | return parse_key_value(self.utils.generalInformation()) 49 | 50 | def parse_security_information(self): 51 | 52 | security_info = parse_key_value(self.utils.securityInformations()) 53 | 54 | # Parse DKIM signature 55 | if 'DKIM Signature' in security_info: 56 | dkim = {} 57 | for part in security_info['DKIM Signature'].split(';'): 58 | if '=' in part: 59 | key, value = part.split('=', 1) 60 | dkim[key.strip()] = value.strip() 61 | security_info['DKIM Signature'] = dkim 62 | 63 | return security_info 64 | 65 | def parse_routing(self): 66 | routing_info = self.utils.routing().split('\n\n') 67 | hops = [] 68 | for hop in re.findall(r'Hop \d+ \|↓\|: (.*)', routing_info[0]): 69 | hop_parts = hop.split(' TO ') 70 | from_part = hop_parts[0].split('FROM ')[1] 71 | to_part, with_part = hop_parts[1].split(' WITH ') 72 | hops.append({ 73 | 'from': from_part, 74 | 'to': to_part, 75 | 'with': with_part 76 | }) 77 | 78 | timestamps = [] 79 | for timestamp in re.findall(r'Hop \d+: (.*)', routing_info[1]): 80 | ts_parts = timestamp.split(', Delta: ') 81 | timestamps.append({ 82 | 'timestamp': ts_parts[0], 83 | 'delta': ts_parts[1] if len(ts_parts) > 1 else None 84 | }) 85 | 86 | return { 87 | 'hops': [dict(hop, **ts) for hop, ts in zip(hops, timestamps)] 88 | } 89 | 90 | def parse_envelope(self): 91 | envelope_info = parse_key_value(self.utils.envelope()) 92 | 93 | # Parse MS Exchange Organization Headers 94 | if 'MS Exchange Organization Headers' in envelope_info: 95 | ms_headers = parse_key_value(envelope_info['MS Exchange Organization Headers']) 96 | envelope_info['MS Exchange Organization'] = ms_headers 97 | del envelope_info['MS Exchange Organization Headers'] 98 | 99 | return envelope_info 100 | 101 | 102 | def parse_spoofing_check(self): 103 | spoofing_info = self.spoofing.spoofing_all_checks().split('\n\n') 104 | result = { 105 | 'smtp_server_mismatch': {}, 106 | 'field_mismatches': {}, 107 | 'external_checks': {} 108 | } 109 | 110 | for section in spoofing_info: 111 | if section.startswith('Checking for SMTP Server Mismatch'): 112 | result['smtp_server_mismatch'] = {'details': section.split('...\n')[1].strip()} 113 | elif section.startswith('Checking for Field Mismatches'): 114 | for line in section.split('\n')[1:]: 115 | if '→' in line: 116 | key, value = line.split('→') 117 | result['field_mismatches'][key.strip()] = value.strip() 118 | elif section.startswith('Checking with VirusTotal'): 119 | result['external_checks']['virustotal'] = parse_key_value(section) 120 | elif section.startswith('Checking with AbuseIPDB'): 121 | result['external_checks']['abuseipdb'] = parse_key_value(section) 122 | elif section.startswith('Checking with IPQualityScore'): 123 | result['external_checks']['ipqualityscore'] = parse_key_value(section) 124 | 125 | return result 126 | 127 | def parse_spoofing_no_dns(self): 128 | spoofing_info = self.spoofing.spoofing_no_dns().split('\n\n') 129 | result = { 130 | 'smtp_server_mismatch': {}, 131 | 'field_mismatches': {}, 132 | 'external_checks': {} 133 | } 134 | 135 | for section in spoofing_info: 136 | if section.startswith('Checking for SMTP Server Mismatch'): 137 | result['smtp_server_mismatch'] = {'details': section.split('...\n')[1].strip()} 138 | elif section.startswith('Checking for Field Mismatches'): 139 | for line in section.split('\n')[1:]: 140 | if '→' in line: 141 | key, value = line.split('→') 142 | result['field_mismatches'][key.strip()] = value.strip() 143 | elif section.startswith('Checking with VirusTotal'): 144 | result['external_checks']['virustotal'] = parse_key_value(section) 145 | elif section.startswith('Checking with AbuseIPDB'): 146 | result['external_checks']['abuseipdb'] = parse_key_value(section) 147 | elif section.startswith('Checking with IPQualityScore'): 148 | result['external_checks']['ipqualityscore'] = parse_key_value(section) 149 | 150 | return result 151 | -------------------------------------------------------------------------------- /core/spoofing.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sys 4 | import requests 5 | import ipaddress 6 | import dns.resolver 7 | from core.utils import Utils 8 | from core.colors import Colors 9 | from email.parser import BytesParser 10 | 11 | class Spoofing: 12 | 13 | def __init__(self, header): 14 | self.eHeader = header 15 | self.colors = Colors() 16 | self.utils = Utils(self.eHeader) 17 | self.resolveIP = self.utils.resolveIP 18 | self.indent = " " 19 | self.resolver = dns.resolver.Resolver() 20 | if 'DRIFTNET_API' not in os.environ: 21 | self.api_key_driftnet = None 22 | else: 23 | self.api_key_driftnet = os.environ['DRIFTNET_API'] # Get your API key here: https://driftnet.io 24 | 25 | #--------------------- This is the "all checks" function. It performs actively DNS resolution. ---------------------# 26 | 27 | def spoofing_all_checks(self): 28 | 29 | report = [] 30 | 31 | try: 32 | with open(self.eHeader, 'r', encoding='UTF-8') as header: 33 | content = BytesParser().parsebytes(header.read().encode('UTF-8')) 34 | except FileNotFoundError: 35 | #print(f'{Fore.RED}File not found.{Fore.RESET}') 36 | self.colors.red("File not found.") 37 | sys.exit(1) 38 | 39 | #print(f'\n{Fore.LIGHTBLUE_EX}Spoofing Check: {Fore.RESET}') 40 | print(self.colors.light_blue("\nSpoofing Check:")) 41 | report.append(f'\nSpoofing Check:\n') 42 | 43 | #------------------------Regex and Field definitions------------------------# 44 | 45 | x = next(iter(reversed(self.utils.getReceivedFields())), None) 46 | if x is not None: 47 | ipv4 = re.findall(r'[\[\(](\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})[\]\)]', x, re.IGNORECASE) 48 | #ipv6 = re.findall(r'[\[\(]([A-Fa-f0-9:]+)[\]\)]', x, re.IGNORECASE) 49 | else: 50 | ipv4 = [] 51 | print(self.colors.red("No 'Received' fields found in the email header.")) 52 | 53 | # get rid of the localhost IP Address 54 | filteredIpv4 = [ip for ip in ipv4 if ip != '127.0.0.1'] 55 | 56 | formatReturnPath = False 57 | formatReplyTo = False 58 | 59 | 60 | fromMatch = re.search(r'<(.*)>', content['from']) 61 | if content['return-path'] is not None: 62 | if '<' in content['return-path']: 63 | returnToPath = re.search(r'<(.*?)>', content['return-path']) 64 | formatReturnPath = True 65 | if content['reply-to'] is not None: 66 | if '<' in content['reply-to']: 67 | replyTo = re.search(r'<(.*)>', content['reply-to']) 68 | formatReplyTo = True 69 | 70 | 71 | #------------------------check for spoofing------------------------# 72 | mx = [] 73 | aRecordsOfMx = [] 74 | 75 | mxAuthResult = [] 76 | aRecordsOfMxAuthResult = [] 77 | authResultOrigIP = None 78 | 79 | # Getting the Domain Name from the "From" Field 80 | if fromMatch is not None: 81 | fromEmailDomain = fromMatch.group(1).split('@')[1] 82 | else: 83 | print(self.colors.white("Could not detect SMTP Server. Manual reviewing required.")) 84 | report.append(f'Could not detect SMTP Server. Manual reviewing required.\n') 85 | 86 | # Getting the MX Records from the Domain Name 87 | try: 88 | getMx = self.resolver.resolve(fromEmailDomain, 'MX') 89 | for servers in getMx: 90 | mx.append(servers.exchange.to_text().lower()) 91 | 92 | except (dns.resolver.LifetimeTimeout, dns.resolver.NoAnswer,dns.resolver.NXDOMAIN,dns.resolver.YXDOMAIN,dns.resolver.NoNameservers): 93 | print(self.colors.red("Could not resolve the MX Record.")) 94 | # Resolving the A Records from the MX Records 95 | for servers in mx: 96 | aRecordsOfMx.append(self.resolveIP(servers)) 97 | 98 | 99 | print(self.colors.magenta("\nChecking for SMTP Server Mismatch...")) 100 | report.append('\nChecking for SMTP Server Mismatch...\n') 101 | 102 | if filteredIpv4: 103 | if filteredIpv4[0] in aRecordsOfMx: 104 | #print(f'{Fore.LIGHTGREEN_EX}No Mismatch detected.{Fore.RESET}') 105 | print(self.colors.green("No Mismatch detected.")) 106 | report.append(f'No Mismatch detected.') 107 | else: 108 | print(self.colors.yellow(f'{self.indent}→ Potential SMTP Server Mismatch detected. Sender SMTP Server is "{fromEmailDomain} [{"".join(filteredIpv4[0])}]" and should be "{fromEmailDomain} [{", ".join(aRecordsOfMx)}]" <- (current MX Record(s) for this domain.)')) 109 | report.append(f'{self.indent}→ Potential SMTP Server Mismatch detected. Sender SMTP Server is "{fromEmailDomain} [{"".join(filteredIpv4[0])}]" and should be "{fromEmailDomain} [{", ".join(aRecordsOfMx)}]" <- (current MX Record(s) for this domain.)') 110 | else: 111 | 112 | #------------------------ If we get no IP addresses from the received from field, we will try to get the IP Address from the "Authentication-Results-Original" Field and do diverse lookups with it. ------------------------# 113 | 114 | # Get the content of the "Authentication-Results-Origin" Field and extract the IPv4 Address which is always after the string "sender IP is " 115 | if isinstance(content['Authentication-Results-Original'], str): 116 | 117 | authResultsOrigin = re.findall(r'sender IP is ([\d.]+)', content['Authentication-Results-Original'], re.IGNORECASE) 118 | if authResultsOrigin: 119 | ipv4 = authResultsOrigin 120 | authResultOrigIP = [ip for ip in ipv4 if ip != '127.0.0.1'] 121 | #print(''.join(authResultIP)) 122 | 123 | # doing a reverse lookup for the authResultOrigIP and getting the domain name 124 | try: 125 | authResultOrigDomain = self.resolver.resolve(dns.reversename.from_address(''.join(authResultOrigIP)), 'PTR') 126 | for domain in authResultOrigDomain: 127 | authResultOrig = domain.to_text().lower() 128 | #print(authResultDomain) 129 | 130 | except (dns.resolver.LifetimeTimeout, dns.resolver.NoAnswer): 131 | #print(f'{Fore.LIGHTRED_EX}Could not resolve the Domain Name.{Fore.RESET}') 132 | print(self.colors.red("Could not resolve the Domain Name.")) 133 | report.append(f'Could not resolve the Domain Name.') 134 | 135 | # Remove the subdomain from the domain name 136 | tmp = authResultOrig.split('.') 137 | authResultFullDomain = '.'.join(tmp[-3:-1]) 138 | 139 | # Get the MX Records of authResultFullDomain 140 | try: 141 | authResultMx = self.resolver.resolve(authResultFullDomain, 'MX') 142 | for servers in authResultMx: 143 | mxAuthResult.append(servers.exchange.to_text().lower()) 144 | 145 | except dns.resolver.LifetimeTimeout: 146 | #print(f'{Fore.LIGHTRED_EX}Could not resolve the MX Record.{Fore.RESET}') 147 | print(self.colors.red("Could not resolve the MX Record.")) 148 | report.append(f'Could not resolve the MX Record.') 149 | 150 | 151 | # Resolving the A Records from mxAuthResult 152 | for n in mxAuthResult: 153 | aRecordsOfMxAuthResult.append(self.resolveIP(n)) 154 | 155 | #print(aRecordsOfMxAuthResult) 156 | #print(aRecordsOfMx, mxAuthResult) 157 | # If one of the values of mxAuthResults is in the mx list, then there is no spoofing. 158 | if any(x in aRecordsOfMxAuthResult for x in aRecordsOfMx): 159 | #print(f'{Fore.LIGHTGREEN_EX}No Mismatch detected.{Fore.RESET}') 160 | print(self.colors.green("No Mismatch detected.")) 161 | report.append(f'No Mismatch detected.') 162 | #print(aRecordsOfMxAuthResult[0]) 163 | 164 | else: 165 | print(self.colors.yellow(f'{self.indent}No IPv4 Address detected in "FROM" Field. Doing additional checks...')) 166 | #report.append(f'{indent}No IPv4 Address detected in "FROM" Field. Doing additional checks...') 167 | report.append(f'{self.indent}No IPv4 Address detected in "FROM" Field. Doing additional checks...') 168 | # If there is no value matching, then we need to retrieve the spf records of mxAuthResults. Extract the values between the quotes. 169 | txtRecords = [] 170 | try: 171 | authResultSpf = self.resolver.resolve(authResultFullDomain, 'TXT') 172 | for spf in authResultSpf: 173 | authResultSpf = spf.to_text().lower() 174 | txtRecords.append(re.findall(r'"(.*?)"', authResultSpf)) 175 | 176 | except dns.resolver.LifetimeTimeout: 177 | #print(f'{Fore.LIGHTRED_EX}Could not resolve the SPF Record.{Fore.RESET}') 178 | print(self.colors.red("Could not resolve the SPF Record.")) 179 | report.append(f'{self.indent}Could not resolve the SPF Record.') 180 | 181 | 182 | # get the subnets from the txtRecords 183 | subnetsTmp = [] 184 | for txt in txtRecords: 185 | for subnet in txt: 186 | # Extract the subnets from the txtRecords 187 | subnetsTmp.append(re.findall(r'ip4:(.*)', subnet)) 188 | 189 | 190 | # if the list subnetsTmp does not contain something with "ip4" (most of cases), then we can continue with the check. If it is empty, then we need to do a lookup with the "include" values from the txtRecords. 191 | if any('ip4:' in subnetss for sublist in subnetsTmp for subnetss in sublist): 192 | # extract the subnets from the list and put them in a new list 193 | subnets = [] 194 | for subnet in subnetsTmp: 195 | for x in subnet: 196 | substrings = x.split(' ') 197 | subnets.extend(s for s in substrings if s.startswith('ip4:')) 198 | 199 | 200 | # convert the subnets to ipaddress objects 201 | ipSubnets = [] 202 | for subnet in subnets: 203 | subnet = subnet.replace('ip4:', '') # Remove 'ip4:' prefix 204 | networks = subnet.split(' ') # Split the string into individual networks 205 | for network in networks: 206 | if '/' in network: # Ignore non-IP strings like '-all' 207 | ipSubnets.append(ipaddress.ip_network(network, strict=False)) 208 | 209 | if any(ipaddress.ip_address(authResultOrigIP[0]) in subnet for subnet in ipSubnets): 210 | print(self.colors.green(f'{self.indent}{self.indent}→ No Mismatch detected.')) 211 | report.append(f'\n{self.indent}{self.indent}→ No Mismatch detected.') 212 | else: 213 | print(self.colors.light_yellow(f'{self.indent}{self.indent}→ Potential Mismatch detected: Authentication-Results-Original ({authResultOrigIP[0]}) NOT IN SPF Record ({subnets})')) 214 | report.append(f'\n{self.indent}{self.indent}→ Potential Mismatch detected: Authentication-Results-Original ({authResultOrigIP[0]}) NOT IN SPF Record ({subnets})') 215 | 216 | 217 | elif not any(subnet for subnet in subnetsTmp): #if subnetTmp is empty, then we can try to get the "include" values from the txtRecords and do a lookup with them. 218 | includeTmp = [] 219 | for txt in txtRecords: 220 | for include in txt: 221 | includeTmp.append(re.findall(r'include:(.*)', include)) 222 | 223 | includeTmp = [x for x in includeTmp if x] 224 | 225 | # Extract value of includeTmp. This shit hurts. 226 | extractionIncludeValue = [x for sublist in includeTmp for x in sublist] 227 | 228 | extractionIncludeValue = [mechanism for string in extractionIncludeValue for mechanism in string.split()] #split this shit 229 | extractionIncludeValue = [mechanism for mechanism in extractionIncludeValue if mechanism not in ["~all", "-all"]] # Remove "~all" and "-all" from extractionIncludeValue 230 | 231 | txtRecordsOfInclude = [] 232 | for include in extractionIncludeValue: 233 | try: 234 | spfResultsInclude = self.resolver.resolve(include, 'TXT') 235 | for spfInclude in spfResultsInclude: 236 | spfResultsInclude = spfInclude.to_text().lower() 237 | txtRecordsOfInclude.append(re.findall(r'"(.*?)"', spfResultsInclude)) 238 | except dns.resolver.LifetimeTimeout: 239 | #print(f'{Fore.LIGHTRED_EX}Could not resolve the SPF Record.{Fore.RESET}') 240 | print(self.colors.red("Could not resolve the SPF Record.")) 241 | report.append('Could not resolve the SPF Record.') 242 | 243 | # Now this shit gets funny 244 | extractionSecondLevel = [y for subsublist in txtRecordsOfInclude for y in subsublist] 245 | 246 | extractionSecondLevel = [technique for idontknow in extractionSecondLevel for technique in idontknow.split()] 247 | extractionSecondLevel = [technique for technique in extractionSecondLevel if technique not in['~all', '-all']] 248 | extractionSecondLevel = [technique for technique in extractionSecondLevel if technique != 'v=spf1'] 249 | 250 | # Extract the domain from the include mechanism 251 | includeDomains = [mechanism.split(':')[1] for mechanism in extractionSecondLevel if mechanism.startswith('include:')] 252 | 253 | txtRecordOfIncludeSecond = [] 254 | for domain in includeDomains: 255 | try: 256 | resultsOfInclude = self.resolver.resolve(domain, 'TXT') 257 | for p in resultsOfInclude: 258 | includeResults = p.to_text().lower() 259 | txtRecordOfIncludeSecond.append(re.findall(r'"(.*?)"', includeResults)) 260 | except dns.resolver.LifetimeTimeout: 261 | #print(f'{Fore.LIGHTRED_EX}Could not resolve the SPF Record.{Fore.RESET}') 262 | print(self.colors.red("Could not resolve the SPF Record.")) 263 | report.append('Could not resolve the SPF Record.') 264 | 265 | 266 | subnetsOfInclude = [] 267 | for b in txtRecordOfIncludeSecond: 268 | for h in b: 269 | subnetsOfInclude.append(re.findall(r'ip4:(.*)', h)) 270 | 271 | if any('ip4:' in subnet for sublist in subnetsOfInclude for subnet in sublist): 272 | #print(f'{Fore.LIGHTYELLOW_EX}{indent}{indent}Getting deeper into the SPF Records...{Fore.RESET}') 273 | print(self.colors.yellow(f'{self.indent}{self.indent}Getting deeper into the SPF Records...')) 274 | report.append(f'\n{self.indent}{self.indent}Getting deeper into the SPF Records...') 275 | # extract the subnets from the list and put them in a new list 276 | subnets = [] 277 | for subnet in subnetsOfInclude: 278 | for x in subnet: 279 | substrings = x.split(' ') 280 | subnets.extend(s for s in substrings if s.startswith('ip4:')) 281 | 282 | # convert the subnets to ipaddress objects 283 | ipSubnets = [] 284 | for subnet in subnets: 285 | subnet = subnet.replace('ip4:', '') # Remove 'ip4:' prefix 286 | networks = subnet.split(' ') 287 | for network in networks: 288 | if '/' in network: 289 | ipSubnets.append(ipaddress.ip_network(network, strict=False)) 290 | 291 | if any(ipaddress.ip_address(authResultOrigIP[0]) in subnet for subnet in ipSubnets): 292 | #print(f'{Fore.LIGHTGREEN_EX}{indent}{indent}→ No Mismatch detected.{Fore.RESET}') 293 | print(self.colors.green(f'{self.indent}{self.indent}→ No Mismatch detected.')) 294 | report.append(f'\n{self.indent}{self.indent}→ No Mismatch detected.') 295 | else: 296 | #print(f'{Fore.LIGHTRED_EX}{indent}{indent}→ Potential Mismatch detected: Authentication-Results-Original ({authResultOrigIP[0]}) NOT IN SPF Record ({subnets}){Fore.RESET}') 297 | print(self.colors.red(f'{self.indent}{self.indent}→ Potential Mismatch detected: Authentication-Results-Original ({authResultOrigIP[0]}) NOT IN SPF Record ({subnets})')) 298 | report.append(f'\n{self.indent}{self.indent}→ Potential Mismatch detected: Authentication-Results-Original ({authResultOrigIP[0]}) NOT IN SPF Record ({subnets})') 299 | 300 | else: 301 | #print(f'{Fore.LIGHTRED_EX}{indent}{indent}→ Could not detect SPF Record. Manual Reviewing required.{Fore.RESET}') 302 | print(self.colors.red(f'{self.indent}{self.indent}→ Could not detect SPF Record. Manual Reviewing required.')) 303 | report.append(f'\n{self.indent}{self.indent}→ Could not detect SPF Record. Manual Reviewing required.') 304 | 305 | else: 306 | #print(f'{Fore.WHITE}Could not detect SMTP Server. Manual reviewing required.{Fore.RESET}') 307 | print(self.colors.white("Could not detect SMTP Server. Manual reviewing required.")) 308 | report.append(f'Could not detect SMTP Server. Manual reviewing required.\n') 309 | 310 | #------------------------Check for Field Mismatches------------------------# 311 | 312 | #print(f'\n{Fore.LIGHTMAGENTA_EX}Checking for Field Mismatches...{Fore.RESET}') 313 | print(self.colors.magenta("\nChecking for Field Mismatches...")) 314 | report.append('\nChecking for Field Mismatches...\n') 315 | 316 | if content['message-id'] is not None: 317 | #print(f'{Fore.LIGHTGREEN_EX}Message-ID Field detected !{Fore.RESET}') 318 | print(self.colors.green("Message-ID Field detected !")) 319 | report.append('Message-ID Field detected !\n') 320 | # Get the domain name between the "<>" brackets and split it at the "@" sign 321 | messageIDDomain = content['message-id'].split('@')[1].split('>')[0] 322 | #print(messageIDDomain) 323 | if fromEmailDomain.strip().lower() != messageIDDomain.strip().lower(): 324 | #print(f'{Fore.LIGHTYELLOW_EX}{indent}→ Suspicious activity detected: Message-ID Domain "{messageIDDomain}" NOT EQUAL "FROM" Domain "{fromEmailDomain}"{Fore.RESET}') 325 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: Message-ID Domain "{messageIDDomain}" NOT EQUAL "FROM" Domain "{fromEmailDomain}"')) 326 | report.append(f'{self.indent}→ Suspicious activity detected: Message-ID Domain ({messageIDDomain}) NOT EQUAL "FROM" Domain ({fromEmailDomain})\n') 327 | else: 328 | #print(f'{Fore.LIGHTGREEN_EX}{indent}→ No Mismatch detected.{Fore.RESET}') 329 | print(self.colors.green(f'{self.indent}→ No Mismatch detected.')) 330 | report.append(f'{self.indent}→ No Mismatch detected.\n') 331 | 332 | else: 333 | #print(f'{Fore.WHITE}No Message-ID Field detected. Skipping...{Fore.RESET}') 334 | print(self.colors.white("No Message-ID Field detected. Skipping...")) 335 | report.append('No Message-ID Field detected. Skipping...\n') 336 | 337 | 338 | if fromMatch.group(1) is not None and content['reply-to'] is not None: 339 | 340 | #print(f'{Fore.LIGHTGREEN_EX}Reply-To Field detected !{Fore.RESET}') 341 | print(self.colors.green("Reply-To Field detected !")) 342 | report.append('Reply-To Field detected !') 343 | 344 | if formatReplyTo == False: 345 | if content['from'].strip().lower() != content['reply-to'].strip().lower(): 346 | #print(f'{Fore.LIGHTYELLOW_EX}{indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({content["reply-to"]}){Fore.RESET}') 347 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({content["reply-to"]})')) 348 | report.append(f'\n{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({content["reply-to"]})') 349 | 350 | else: 351 | #print(f'{Fore.LIGHTGREEN_EX}{indent}→ No "FROM - REPLY-TO" Mismatch detected.{Fore.RESET}') 352 | print(self.colors.green(f'{self.indent}→ No "FROM - REPLY-TO" Mismatch detected.')) 353 | report.append(f'{self.indent}→ No "FROM - REPLY-TO" Mismatch detected.') 354 | 355 | elif formatReplyTo == True: 356 | if fromMatch.group(1).strip().lower() != replyTo.group(1).strip().lower(): 357 | #print(f'{Fore.LIGHTYELLOW_EX}{indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({replyTo.group(1)}){Fore.RESET}') 358 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({replyTo.group(1)})')) 359 | report.append(f'\n{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({replyTo.group(1)})') 360 | 361 | else: 362 | #print(f'{Fore.LIGHTGREEN_EX}{indent}→ No "FROM - REPLY-TO" Mismatch detected.{Fore.RESET}') 363 | print(self.colors.green(f'{self.indent}→ No "FROM - REPLY-TO" Mismatch detected.')) 364 | report.append(f'{self.indent}→ No "FROM - REPLY-TO" Mismatch detected.') 365 | 366 | else: 367 | #print(f'{Fore.WHITE}No Reply-To Field detected. Skipping...{Fore.RESET}') 368 | print(self.colors.white("No Reply-To Field detected. Skipping...")) 369 | report.append('No Reply-To Field detected. Skipping...\n') 370 | 371 | if fromMatch.group(1) is not None and content['return-path'] is not None: 372 | 373 | #print(f'{Fore.LIGHTGREEN_EX}Return-Path Field detected !{Fore.RESET}') 374 | print(self.colors.green("Return-Path Field detected !")) 375 | report.append('\nReturn-Path Field detected !') 376 | 377 | if formatReturnPath == False: 378 | if fromMatch.group(1).strip().lower() != content['return-path'].strip().lower(): 379 | #print(f'{Fore.LIGHTYELLOW_EX}{indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "RETURN-PATH" Field ({content["return-path"]}){Fore.RESET}') 380 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "RETURN-PATH" Field ({content["return-path"]})')) 381 | report.append(f'\n{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "RETURN-PATH" Field ({content["return-path"]})') 382 | 383 | else: 384 | print(self.colors.green(f'{self.indent}→ No "FROM - RETURN-PATH" Mismatch detected.')) 385 | report.append(f'\n{self.indent}→ No "FROM - RETURN-PATH" Mismatch detected.') 386 | 387 | elif formatReturnPath == True: 388 | if fromMatch.group(1).strip().lower() != returnToPath.group(1).strip().lower(): 389 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "RETURN-PATH" Field ({returnToPath.group(1)})')) 390 | report.append(f'\n{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "RETURN-PATH" Field ({returnToPath.group(1)})') 391 | 392 | else: 393 | print(self.colors.green(f'{self.indent}→ No "FROM - RETURN-PATH" Mismatch detected.')) 394 | report.append(f'\n{self.indent}→ No "FROM - RETURN-PATH" Mismatch detected.') 395 | 396 | else: 397 | print(self.colors.white("No Return-Path Field detected. Skipping...")) 398 | report.append('No Return-Path Field detected. Skipping...') 399 | 400 | #------------------------Check with VirusTotal------------------------# 401 | 402 | #print(f'\n{Fore.LIGHTYELLOW_EX}Note: You can use your own VirusTotal, AbuseIPDB and IPQualityScore API Key to generate a report for the IP Address. Check the Source Code.{Fore.RESET}') 403 | print(self.colors.yellow("\nNote: You can use your own VirusTotal, AbuseIPDB and IPQualityScore API Key to generate a report for the IP Address. Check the Source Code.")) 404 | 405 | print(self.colors.magenta("\nChecking with VirusTotal...")) 406 | report.append('\n\nChecking with VirusTotal...\n') 407 | 408 | if filteredIpv4: 409 | # If you got an VT API Key, you can use it here. It will generate a report for the IP Address. Uncomment the line under this comment and replace with your API Key. 410 | #os.system(f'curl -s -X GET --header "x-apikey: " "https://www.virustotal.com/api/v3/ip_addresses/{ipv4[0]}" > vt.json') 411 | 412 | print(f'Detections: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/detection")}') 413 | print(f'Relations: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/relations")}') 414 | print(f'Graph: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/graph")}') 415 | print(f'Network Traffic: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/network-traffic")}') 416 | print(f'WHOIS: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/whois")}') 417 | print(f'Comments: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/comments")}') 418 | print(f'Votes: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/votes")}') 419 | 420 | report.append(f'Detections: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/detection\n') 421 | report.append(f'Relations: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/relations\n') 422 | report.append(f'Graph: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/graph\n') 423 | report.append(f'Network Traffic: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/network-traffic\n') 424 | report.append(f'WHOIS: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/whois\n') 425 | report.append(f'Comments: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/comments\n') 426 | report.append(f'Votes: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/votes\n') 427 | 428 | 429 | elif authResultOrigIP: 430 | print(f'Detections: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/detection")}'), 431 | print(f'Relations: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/relations")}') 432 | print(f'Graph: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/graph")}') 433 | print(f'Network Traffic: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/network-traffic")}') 434 | print(f'WHOIS: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/whois")}') 435 | print(f'Comments: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/comments")}') 436 | print(f'Votes: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/votes")}') 437 | 438 | report.append(f'Detections: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/detection\n') 439 | report.append(f'Relations: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/relations\n') 440 | report.append(f'Graph: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/graph\n') 441 | report.append(f'Network Traffic: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/network-traffic\n') 442 | report.append(f'WHOIS: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/whois\n') 443 | report.append(f'Comments: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/comments\n') 444 | report.append(f'Votes: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/votes\n') 445 | 446 | 447 | 448 | else: 449 | #print(f'{Fore.WHITE}Could not detect SMTP Server. Manual reviewing required.{Fore.RESET}') 450 | print(self.colors.white("Could not detect SMTP Server. Manual reviewing required.")) 451 | report.append(f'Could not detect SMTP Server. Manual reviewing required.') 452 | 453 | #------------------------Check with AbuseIPDB------------------------# 454 | 455 | print(self.colors.magenta("\nChecking with AbuseIPDB...")) 456 | report.append('\nChecking with AbuseIPDB...\n') 457 | 458 | if filteredIpv4: 459 | # If you got an AbuseIPDB API Key, you can use it here. It will generate a report for the IP Address. Uncomment the line under this comment and replace with your API Key. 460 | #os.system(f'curl -s -X GET --header "Key: " "https://api.abuseipdb.com/api/v2/check?ipAddress={ipv4[0]}" > abuseipdb.json') 461 | print(f'AbuseIPDB: {self.colors.green(f"https://www.abuseipdb.com/check/{filteredIpv4[0]}")}') 462 | report.append(f'AbuseIPDB: https://www.abuseipdb.com/check/{filteredIpv4[0]}') 463 | 464 | elif authResultOrigIP: 465 | print(f'AbuseIPDB: {self.colors.green(f"https://www.abuseipdb.com/check/{authResultOrigIP[0]}")}') 466 | report.append(f'AbuseIPDB: https://www.abuseipdb.com/check/{authResultOrigIP[0]}') 467 | 468 | else: 469 | print(self.colors.white("Could not detect SMTP Server. Manual reviewing required.")) 470 | report.append(f'Could not detect SMTP Server. Manual reviewing required.') 471 | 472 | #------------------------Check with IPQualityScore------------------------# 473 | 474 | #print(f'\n{Fore.LIGHTMAGENTA_EX}Checking with IPQualityScore...{Fore.RESET}') 475 | print(self.colors.magenta("\nChecking with IPQualityScore...")) 476 | report.append('\n\nChecking with IPQualityScore...\n') 477 | 478 | if filteredIpv4: 479 | # If you got an IPQualityScore API Key, you can use it here. It will generate a report for the IP Address. Uncomment the line under this comment and replace with your API Key. 480 | #os.system(f'curl -s -X GET --header "Key: " "https://www.ipqualityscore.com/api/json/ip//{ipv4[0]}" > ipqualityscore.json') 481 | #print(f'IPQualityScore: {Fore.LIGHTGREEN_EX}https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{filteredIpv4[0]}{Fore.RESET}') 482 | print(f'IPQualityScore: {self.colors.green(f"https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{filteredIpv4[0]}")}') 483 | report.append(f'IPQualityScore: https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{filteredIpv4[0]}') 484 | 485 | elif authResultOrigIP: 486 | #print(f'IPQualityScore: {Fore.LIGHTGREEN_EX}https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{authResultOrigIP[0]}{Fore.RESET}') 487 | print(f'IPQualityScore: {self.colors.green(f"https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{authResultOrigIP[0]}")}') 488 | report.append(f'IPQualityScore: https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{authResultOrigIP[0]}') 489 | 490 | else: 491 | #print(f'{Fore.WHITE}Could not detect SMTP Server. Manual reviewing required.{Fore.RESET}') 492 | print(self.colors.white("Could not detect SMTP Server. Manual reviewing required.")) 493 | report.append(f'Could not detect SMTP Server. Manual reviewing required.') 494 | 495 | return ''.join(report) 496 | 497 | 498 | #------------------------This part is for the passive DNS check.------------------------# 499 | 500 | # Function for fetching A Records via the Driftnet API 501 | def passive_a_records_driftnet(self, mx_server) -> list: 502 | 503 | a_records = [] 504 | url = f"https://api.driftnet.io/v1/domain/fdns?host={mx_server}" 505 | 506 | headers = { 507 | 'Authorization': f'Bearer {self.api_key_driftnet}', 508 | 'Content-Type': 'application/json' 509 | } 510 | 511 | response = requests.get(url, headers=headers) 512 | if response.status_code == 200: 513 | 514 | parsed_json = response.json() 515 | 516 | # First iteration is for parsing the JSON structure first 517 | for x in parsed_json['results']: 518 | # Second iteration is for getting the 'items' key 519 | for y in x['items']: 520 | # Third iteration is for filtering the results and getting the exact host value that matches the mx_server variable 521 | if y['value'] == mx_server: 522 | # Fourth iteration is iterating again over the 'items' key, but this time we have filtered it and we can get the context key that equals to 'dns-a 523 | for z in x['items']: 524 | if z['context'] == 'dns-a': 525 | a_records.append(z['value']) 526 | 527 | return a_records 528 | 529 | 530 | else: 531 | #print(self.colors.red(f"[DEBUG] Error getting A records for {mx_server}: {response.status_code}")) 532 | return None 533 | 534 | # Funtion for fetching MX Records via the Driftnet API 535 | def passive_mx_records_driftnet(self, domain) -> list: 536 | 537 | results = [] 538 | 539 | try: 540 | url = f"https://api.driftnet.io/v1/domain/mx?host={domain}" 541 | 542 | headers = { 543 | 'Authorization': f'Bearer {self.api_key_driftnet}', 544 | 'Content-Type': 'application/json' 545 | } 546 | 547 | # Doing the actual request 548 | response = requests.get(url, headers=headers) 549 | if response.status_code == 200: 550 | 551 | parsed_json = response.json() 552 | 553 | for x in parsed_json['results']: 554 | for y in x['items']: 555 | if y['type'] == 'host': 556 | for z in x['items']: 557 | if z['context'] == 'dns-mx' and z['type'] == 'host': 558 | results.append(z['value']) 559 | 560 | 561 | # filtering out the duplicates from results 562 | return list(set(results)) 563 | 564 | except Exception as e: 565 | print(self.colors.red(f"Error getting MX records for {domain}: {str(e)}")) 566 | return[] 567 | 568 | 569 | # Revere DNS lookup via the Driftnet API 570 | def passive_reverse_dns_driftnet(self, ip) -> list: 571 | url = f"https://api.driftnet.io/v1/domain/rdns?ip={ip}" 572 | 573 | headers = { 574 | 'Authorization': f'Bearer {self.api_key_driftnet}', 575 | 'Content-Type': 'application/json' 576 | } 577 | 578 | try: 579 | response = requests.get(url, headers=headers) 580 | if response.status_code == 200: 581 | parsed_data = self.parse_driftnet_response(response.json()) 582 | return parsed_data['ptr_records'] 583 | else: 584 | print(self.colors.red(f"Error getting reverse DNS for {ip}: {response.status_code}")) 585 | return [] 586 | except Exception as e: 587 | print(self.colors.red(f"Error getting reverse DNS for {ip}: {str(e)}")) 588 | return [] 589 | 590 | 591 | @staticmethod 592 | def parse_driftnet_response(data) -> dict: 593 | try: 594 | values = [] 595 | ptr_records = [] 596 | 597 | for result in data.get('results', []): 598 | for item in result.get('items', []): 599 | value = item.get('value') 600 | if value: 601 | values.append(value) 602 | if item.get('context') == 'dns-ptr': 603 | ptr_records.append(value) 604 | 605 | return { 606 | 'all_values': values, 607 | 'ptr_records': ptr_records 608 | } 609 | 610 | except KeyError as e: 611 | print(f"Error: Missing expected key in JSON structure: {e}") 612 | return {'all_values': [], 'ptr_records': []} 613 | 614 | 615 | def spoofing_passive_dns(self): 616 | 617 | report = [] 618 | 619 | try: 620 | with open(self.eHeader, 'r', encoding='UTF-8') as header: 621 | content = BytesParser().parsebytes(header.read().encode('UTF-8')) 622 | except FileNotFoundError: 623 | self.colors.red("File not found.") 624 | sys.exit(1) 625 | 626 | print(self.colors.light_blue("\nSpoofing Check:")) 627 | report.append(f'\nSpoofing Check:\n') 628 | 629 | #------------------------Regex and Field definitions------------------------# 630 | 631 | x = next(iter(reversed(self.utils.getReceivedFields())), None) 632 | if x is not None: 633 | ipv4 = re.findall(r'[\[\(](\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})[\]\)]', x, re.IGNORECASE) 634 | #ipv6 = re.findall(r'[\[\(]([A-Fa-f0-9:]+)[\]\)]', x, re.IGNORECASE) 635 | else: 636 | ipv4 = [] 637 | print(self.colors.red("No 'Received' fields found in the email header.")) 638 | 639 | # get rid of the localhost IP Address 640 | filteredIpv4 = [ip for ip in ipv4 if ip != '127.0.0.1'] 641 | 642 | formatReturnPath = False 643 | formatReplyTo = False 644 | 645 | 646 | fromMatch = re.search(r'<(.*)>', content['from']) 647 | if content['return-path'] is not None: 648 | if '<' in content['return-path']: 649 | returnToPath = re.search(r'<(.*?)>', content['return-path']) 650 | formatReturnPath = True 651 | if content['reply-to'] is not None: 652 | if '<' in content['reply-to']: 653 | replyTo = re.search(r'<(.*)>', content['reply-to']) 654 | formatReplyTo = True 655 | 656 | 657 | #------------------------check for spoofing------------------------# 658 | aRecordsOfMxAuthResult = [] 659 | authResultOrigIP = None 660 | 661 | # Getting the Domain Name from the "From" Field 662 | if fromMatch is not None: 663 | fromEmailDomain = fromMatch.group(1).split('@')[1] 664 | else: 665 | print(self.colors.white("Could not detect SMTP Server. Manual reviewing required.")) 666 | report.append(f'Could not detect SMTP Server. Manual reviewing required.\n') 667 | 668 | 669 | #print('Email Domain:', fromEmailDomain) # Debug statement 670 | 671 | DF_MX_A_RECORD = self.passive_a_records_driftnet(fromEmailDomain) 672 | if DF_MX_A_RECORD is None: 673 | print(self.colors.red("Could not retrieve data from Driftnet.")) 674 | return ''.join(report) 675 | 676 | #print('A Records:', DF_MX_A_RECORD) # Debug statement 677 | DF_MX_A_RECORD = list(dict.fromkeys(DF_MX_A_RECORD)) 678 | 679 | print(self.colors.light_magenta("\nChecking for SMTP Server Mismatch...")) 680 | report.append(f'\nChecking for SMTP Server Mismatch...\n') 681 | 682 | if filteredIpv4: 683 | for tmp in DF_MX_A_RECORD: 684 | if filteredIpv4[0] in tmp: 685 | print(self.colors.green(f'{self.indent}→ No Mismatch detected.')) 686 | report.append(f'{self.indent}→ No Mismatch detected.') 687 | else: 688 | print(self.colors.yellow(f'{self.indent}→ Potential SMTP Server Mismatch detected. Sender SMTP Server is {fromEmailDomain} [{filteredIpv4[0]}] and should be {tmp} <- (current MX Record(s) for this domain)')) 689 | report.append(f'{self.indent}→ Suspicious activity detected: SMTP Server Mismatch detected.') 690 | 691 | else: 692 | if isinstance(content['Authentication-Results-Original'], str): 693 | authResultsOrigin = re.findall(r'sender IP is ([\d.]+)', content['Authentication-Results-Original'], re.IGNORECASE) 694 | if authResultsOrigin: 695 | ipv4 = authResultsOrigin 696 | authResultOrigIP = [ip for ip in ipv4 if ip != '127.0.0.1'] 697 | 698 | else: 699 | print(self.colors.red("No 'Authentication-Results-Original' Header found. Manual reviewing required.")) 700 | 701 | #print(authResultOrigIP) # Debug statement 702 | if authResultOrigIP: 703 | try: 704 | for domain in authResultOrigIP: 705 | associated_domain = self.passive_reverse_dns_driftnet(domain) 706 | # filtering duplicates 707 | associated_domain = list(dict.fromkeys(associated_domain)) 708 | #print(associated_domain) # Debug statement 709 | 710 | 711 | for subdomain in associated_domain: 712 | tmp = subdomain.split('.') 713 | authResultFullDomain = '.'.join(tmp[-2:]) 714 | #print(authResultFullDomain) # Debug statement 715 | 716 | mx_records_from_df = self.passive_mx_records_driftnet(authResultFullDomain) 717 | #print(mx_records_from_df) # Debug statement 718 | 719 | for x in mx_records_from_df: 720 | aRecordsOfMxAuthResult.append(self.passive_a_records_driftnet(x)) 721 | #print(aRecordsOfMxAuthResult) # Debug statement 722 | 723 | # Flattening the list 724 | if aRecordsOfMxAuthResult: 725 | aRecordsOfMxAuthResult = [item for sublist in aRecordsOfMxAuthResult if sublist is not None for item in sublist] 726 | 727 | #print(aRecordsOfMxAuthResult) # Debug statement 728 | if any(x in aRecordsOfMxAuthResult for x in DF_MX_A_RECORD): 729 | print(self.colors.green(f'{self.indent}→ No Mismatch detected.')) 730 | report.append(f'{self.indent}→ No Mismatch detected.') 731 | 732 | else: 733 | print(self.colors.light_yellow(f'{self.indent}Could not compare values. Manual reviewing required...')) 734 | report.append(f'{self.indent}Could not compare values. Manual reviewing required...') 735 | 736 | except Exception as e: 737 | print(self.colors.red(f"An error occurred while querying the API: {str(e)}")) 738 | 739 | else: 740 | pass 741 | 742 | #------------------------Check for Field Mismatches------------------------# 743 | 744 | print(self.colors.magenta("\nChecking for Field Mismatches...")) 745 | report.append('\nChecking for Field Mismatches...\n') 746 | 747 | if content['message-id'] is not None: 748 | print(self.colors.green("Message-ID Field detected !")) 749 | report.append('Message-ID Field detected !\n') 750 | # Get the domain name between the "<>" brackets and split it at the "@" sign 751 | messageIDDomain = content['message-id'].split('@')[1].split('>')[0] 752 | #print(messageIDDomain) 753 | if fromEmailDomain.strip().lower() != messageIDDomain.strip().lower(): 754 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: Message-ID Domain "{messageIDDomain}" NOT EQUAL "FROM" Domain "{fromEmailDomain}"')) 755 | report.append(f'{self.indent}→ Suspicious activity detected: Message-ID Domain ({messageIDDomain}) NOT EQUAL "FROM" Domain ({fromEmailDomain})\n') 756 | else: 757 | print(self.colors.green(f'{self.indent}→ No Mismatch detected.')) 758 | report.append(f'{self.indent}→ No Mismatch detected.\n') 759 | 760 | else: 761 | print(self.colors.white("No Message-ID Field detected. Skipping...")) 762 | report.append('No Message-ID Field detected. Skipping...\n') 763 | 764 | 765 | if fromMatch.group(1) is not None and content['reply-to'] is not None: 766 | 767 | print(self.colors.green("Reply-To Field detected !")) 768 | report.append('Reply-To Field detected !') 769 | 770 | if formatReplyTo == False: 771 | if content['from'].strip().lower() != content['reply-to'].strip().lower(): 772 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({content["reply-to"]})')) 773 | report.append(f'\n{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({content["reply-to"]})') 774 | 775 | else: 776 | print(self.colors.green(f'{self.indent}→ No "FROM - REPLY-TO" Mismatch detected.')) 777 | report.append(f'{self.indent}→ No "FROM - REPLY-TO" Mismatch detected.') 778 | 779 | elif formatReplyTo == True: 780 | if fromMatch.group(1).strip().lower() != replyTo.group(1).strip().lower(): 781 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({replyTo.group(1)})')) 782 | report.append(f'\n{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({replyTo.group(1)})') 783 | 784 | else: 785 | print(self.colors.green(f'{self.indent}→ No "FROM - REPLY-TO" Mismatch detected.')) 786 | report.append(f'{self.indent}→ No "FROM - REPLY-TO" Mismatch detected.') 787 | 788 | else: 789 | print(self.colors.white("No Reply-To Field detected. Skipping...")) 790 | report.append('No Reply-To Field detected. Skipping...\n') 791 | 792 | if fromMatch.group(1) is not None and content['return-path'] is not None: 793 | 794 | print(self.colors.green("Return-Path Field detected !")) 795 | report.append('\nReturn-Path Field detected !') 796 | 797 | if formatReturnPath == False: 798 | if fromMatch.group(1).strip().lower() != content['return-path'].strip().lower(): 799 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "RETURN-PATH" Field ({content["return-path"]})')) 800 | report.append(f'\n{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "RETURN-PATH" Field ({content["return-path"]})') 801 | 802 | else: 803 | print(self.colors.green(f'{self.indent}→ No "FROM - RETURN-PATH" Mismatch detected.')) 804 | report.append(f'\n{self.indent}→ No "FROM - RETURN-PATH" Mismatch detected.') 805 | 806 | elif formatReturnPath == True: 807 | if fromMatch.group(1).strip().lower() != returnToPath.group(1).strip().lower(): 808 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "RETURN-PATH" Field ({returnToPath.group(1)})')) 809 | report.append(f'\n{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "RETURN-PATH" Field ({returnToPath.group(1)})') 810 | 811 | else: 812 | print(self.colors.green(f'{self.indent}→ No "FROM - RETURN-PATH" Mismatch detected.')) 813 | report.append(f'\n{self.indent}→ No "FROM - RETURN-PATH" Mismatch detected.') 814 | 815 | else: 816 | print(self.colors.white("No Return-Path Field detected. Skipping...")) 817 | report.append('No Return-Path Field detected. Skipping...') 818 | 819 | #------------------------Check with VirusTotal------------------------# 820 | 821 | #print(f'\n{Fore.LIGHTYELLOW_EX}Note: You can use your own VirusTotal, AbuseIPDB and IPQualityScore API Key to generate a report for the IP Address. Check the Source Code.{Fore.RESET}') 822 | print(self.colors.yellow("\nNote: You can use your own VirusTotal, AbuseIPDB and IPQualityScore API Key to generate a report for the IP Address. Check the Source Code.")) 823 | 824 | print(self.colors.magenta("\nChecking with VirusTotal...")) 825 | report.append('\n\nChecking with VirusTotal...\n') 826 | 827 | if filteredIpv4: 828 | # If you got an VT API Key, you can use it here. It will generate a report for the IP Address. Uncomment the line under this comment and replace with your API Key. 829 | #os.system(f'curl -s -X GET --header "x-apikey: " "https://www.virustotal.com/api/v3/ip_addresses/{ipv4[0]}" > vt.json') 830 | 831 | print(f'Detections: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/detection")}') 832 | print(f'Relations: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/relations")}') 833 | print(f'Graph: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/graph")}') 834 | print(f'Network Traffic: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/network-traffic")}') 835 | print(f'WHOIS: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/whois")}') 836 | print(f'Comments: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/comments")}') 837 | print(f'Votes: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/votes")}') 838 | 839 | report.append(f'Detections: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/detection\n') 840 | report.append(f'Relations: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/relations\n') 841 | report.append(f'Graph: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/graph\n') 842 | report.append(f'Network Traffic: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/network-traffic\n') 843 | report.append(f'WHOIS: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/whois\n') 844 | report.append(f'Comments: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/comments\n') 845 | report.append(f'Votes: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/votes\n') 846 | 847 | 848 | elif authResultOrigIP: 849 | print(f'Detections: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/detection")}'), 850 | print(f'Relations: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/relations")}') 851 | print(f'Graph: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/graph")}') 852 | print(f'Network Traffic: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/network-traffic")}') 853 | print(f'WHOIS: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/whois")}') 854 | print(f'Comments: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/comments")}') 855 | print(f'Votes: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/votes")}') 856 | 857 | report.append(f'Detections: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/detection\n') 858 | report.append(f'Relations: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/relations\n') 859 | report.append(f'Graph: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/graph\n') 860 | report.append(f'Network Traffic: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/network-traffic\n') 861 | report.append(f'WHOIS: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/whois\n') 862 | report.append(f'Comments: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/comments\n') 863 | report.append(f'Votes: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/votes\n') 864 | 865 | 866 | 867 | else: 868 | #print(f'{Fore.WHITE}Could not detect SMTP Server. Manual reviewing required.{Fore.RESET}') 869 | print(self.colors.white("Could not detect SMTP Server. Manual reviewing required.")) 870 | report.append(f'Could not detect SMTP Server. Manual reviewing required.') 871 | 872 | #------------------------Check with AbuseIPDB------------------------# 873 | 874 | print(self.colors.magenta("\nChecking with AbuseIPDB...")) 875 | report.append('\nChecking with AbuseIPDB...\n') 876 | 877 | if filteredIpv4: 878 | # If you got an AbuseIPDB API Key, you can use it here. It will generate a report for the IP Address. Uncomment the line under this comment and replace with your API Key. 879 | #os.system(f'curl -s -X GET --header "Key: " "https://api.abuseipdb.com/api/v2/check?ipAddress={ipv4[0]}" > abuseipdb.json') 880 | print(f'AbuseIPDB: {self.colors.green(f"https://www.abuseipdb.com/check/{filteredIpv4[0]}")}') 881 | report.append(f'AbuseIPDB: https://www.abuseipdb.com/check/{filteredIpv4[0]}') 882 | 883 | elif authResultOrigIP: 884 | print(f'AbuseIPDB: {self.colors.green(f"https://www.abuseipdb.com/check/{authResultOrigIP[0]}")}') 885 | report.append(f'AbuseIPDB: https://www.abuseipdb.com/check/{authResultOrigIP[0]}') 886 | 887 | else: 888 | print(self.colors.white("Could not detect SMTP Server. Manual reviewing required.")) 889 | report.append(f'Could not detect SMTP Server. Manual reviewing required.') 890 | 891 | #------------------------Check with IPQualityScore------------------------# 892 | 893 | #print(f'\n{Fore.LIGHTMAGENTA_EX}Checking with IPQualityScore...{Fore.RESET}') 894 | print(self.colors.magenta("\nChecking with IPQualityScore...")) 895 | report.append('\n\nChecking with IPQualityScore...\n') 896 | 897 | if filteredIpv4: 898 | # If you got an IPQualityScore API Key, you can use it here. It will generate a report for the IP Address. Uncomment the line under this comment and replace with your API Key. 899 | #os.system(f'curl -s -X GET --header "Key: " "https://www.ipqualityscore.com/api/json/ip//{ipv4[0]}" > ipqualityscore.json') 900 | #print(f'IPQualityScore: {Fore.LIGHTGREEN_EX}https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{filteredIpv4[0]}{Fore.RESET}') 901 | print(f'IPQualityScore: {self.colors.green(f"https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{filteredIpv4[0]}")}') 902 | report.append(f'IPQualityScore: https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{filteredIpv4[0]}') 903 | 904 | elif authResultOrigIP: 905 | #print(f'IPQualityScore: {Fore.LIGHTGREEN_EX}https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{authResultOrigIP[0]}{Fore.RESET}') 906 | print(f'IPQualityScore: {self.colors.green(f"https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{authResultOrigIP[0]}")}') 907 | report.append(f'IPQualityScore: https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{authResultOrigIP[0]}') 908 | 909 | else: 910 | #print(f'{Fore.WHITE}Could not detect SMTP Server. Manual reviewing required.{Fore.RESET}') 911 | print(self.colors.white("Could not detect SMTP Server. Manual reviewing required.")) 912 | report.append(f'Could not detect SMTP Server. Manual reviewing required.') 913 | 914 | return ''.join(report) 915 | 916 | 917 | #------------------------This part is for the NO DNS check.------------------------# 918 | 919 | def spoofing_no_dns(self): 920 | 921 | report = [] 922 | 923 | try: 924 | with open(self.eHeader, 'r', encoding='UTF-8') as header: 925 | content = BytesParser().parsebytes(header.read().encode('UTF-8')) 926 | except FileNotFoundError: 927 | self.colors.red("File not found.") 928 | sys.exit(1) 929 | 930 | print(self.colors.light_red("\nNo DNS resolution is performed. This heavily affects the results !")) 931 | report.append(f'No DNS resolution is performed. This heavily affects the results !!\n') 932 | 933 | print(self.colors.light_blue("\nSpoofing Check:")) 934 | report.append(f'\nSpoofing Check:\n') 935 | 936 | #------------------------Regex and Field definitions------------------------# 937 | 938 | x = next(iter(reversed(self.utils.getReceivedFields())), None) 939 | if x is not None: 940 | ipv4 = re.findall(r'[\[\(](\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})[\]\)]', x, re.IGNORECASE) 941 | ipv6 = re.findall(r'[\[\(]([A-Fa-f0-9:]+)[\]\)]', x, re.IGNORECASE) 942 | 943 | else: 944 | print(self.colors.red("No 'Received' fields found in the email header.")) 945 | ipv4 = [] 946 | # get rid of the localhost IP Address 947 | filteredIpv4 = [ip for ip in ipv4 if ip != '127.0.0.1'] 948 | 949 | formatReturnPath = False 950 | formatReplyTo = False 951 | 952 | 953 | fromMatch = re.search(r'<(.*)>', content['from']) 954 | if content['return-path'] is not None: 955 | if '<' in content['return-path']: 956 | returnToPath = re.search(r'<(.*?)>', content['return-path']) 957 | formatReturnPath = True 958 | if content['reply-to'] is not None: 959 | if '<' in content['reply-to']: 960 | replyTo = re.search(r'<(.*)>', content['reply-to']) 961 | formatReplyTo = True 962 | 963 | 964 | #------------------------check for spoofing------------------------# 965 | authResultOrigIP = None 966 | 967 | # Getting the Domain Name from the "From" Field 968 | if fromMatch is not None: 969 | fromEmailDomain = fromMatch.group(1).split('@')[1] 970 | else: 971 | print(self.colors.white("Could not detect SMTP Server. Manual reviewing required.")) 972 | report.append(f'Could not detect SMTP Server. Manual reviewing required.\n') 973 | 974 | 975 | #------------------------Check for Field Mismatches------------------------# 976 | 977 | #print(f'\n{Fore.LIGHTMAGENTA_EX}Checking for Field Mismatches...{Fore.RESET}') 978 | print(self.colors.magenta("\nChecking for Field Mismatches...")) 979 | report.append('\nChecking for Field Mismatches...\n') 980 | 981 | if content['message-id'] is not None: 982 | #print(f'{Fore.LIGHTGREEN_EX}Message-ID Field detected !{Fore.RESET}') 983 | print(self.colors.green("Message-ID Field detected !")) 984 | report.append('Message-ID Field detected !\n') 985 | # Get the domain name between the "<>" brackets and split it at the "@" sign 986 | messageIDDomain = content['message-id'].split('@')[1].split('>')[0] 987 | #print(messageIDDomain) 988 | if fromEmailDomain.strip().lower() != messageIDDomain.strip().lower(): 989 | #print(f'{Fore.LIGHTYELLOW_EX}{indent}→ Suspicious activity detected: Message-ID Domain "{messageIDDomain}" NOT EQUAL "FROM" Domain "{fromEmailDomain}"{Fore.RESET}') 990 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: Message-ID Domain "{messageIDDomain}" NOT EQUAL "FROM" Domain "{fromEmailDomain}"')) 991 | report.append(f'{self.indent}→ Suspicious activity detected: Message-ID Domain ({messageIDDomain}) NOT EQUAL "FROM" Domain ({fromEmailDomain})\n') 992 | else: 993 | #print(f'{Fore.LIGHTGREEN_EX}{indent}→ No Mismatch detected.{Fore.RESET}') 994 | print(self.colors.green(f'{self.indent}→ No Mismatch detected.')) 995 | report.append(f'{self.indent}→ No Mismatch detected.\n') 996 | 997 | else: 998 | #print(f'{Fore.WHITE}No Message-ID Field detected. Skipping...{Fore.RESET}') 999 | print(self.colors.white("No Message-ID Field detected. Skipping...")) 1000 | report.append('No Message-ID Field detected. Skipping...\n') 1001 | 1002 | 1003 | if fromMatch.group(1) is not None and content['reply-to'] is not None: 1004 | 1005 | #print(f'{Fore.LIGHTGREEN_EX}Reply-To Field detected !{Fore.RESET}') 1006 | print(self.colors.green("Reply-To Field detected !")) 1007 | report.append('Reply-To Field detected !') 1008 | 1009 | if formatReplyTo == False: 1010 | if content['from'].strip().lower() != content['reply-to'].strip().lower(): 1011 | #print(f'{Fore.LIGHTYELLOW_EX}{indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({content["reply-to"]}){Fore.RESET}') 1012 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({content["reply-to"]})')) 1013 | report.append(f'\n{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({content["reply-to"]})') 1014 | 1015 | else: 1016 | #print(f'{Fore.LIGHTGREEN_EX}{indent}→ No "FROM - REPLY-TO" Mismatch detected.{Fore.RESET}') 1017 | print(self.colors.green(f'{self.indent}→ No "FROM - REPLY-TO" Mismatch detected.')) 1018 | report.append(f'{self.indent}→ No "FROM - REPLY-TO" Mismatch detected.') 1019 | 1020 | elif formatReplyTo == True: 1021 | if fromMatch.group(1).strip().lower() != replyTo.group(1).strip().lower(): 1022 | #print(f'{Fore.LIGHTYELLOW_EX}{indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({replyTo.group(1)}){Fore.RESET}') 1023 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({replyTo.group(1)})')) 1024 | report.append(f'\n{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "REPLY-TO" Field ({replyTo.group(1)})') 1025 | 1026 | else: 1027 | #print(f'{Fore.LIGHTGREEN_EX}{indent}→ No "FROM - REPLY-TO" Mismatch detected.{Fore.RESET}') 1028 | print(self.colors.green(f'{self.indent}→ No "FROM - REPLY-TO" Mismatch detected.')) 1029 | report.append(f'{self.indent}→ No "FROM - REPLY-TO" Mismatch detected.') 1030 | 1031 | else: 1032 | #print(f'{Fore.WHITE}No Reply-To Field detected. Skipping...{Fore.RESET}') 1033 | print(self.colors.white("No Reply-To Field detected. Skipping...")) 1034 | report.append('No Reply-To Field detected. Skipping...\n') 1035 | 1036 | if fromMatch.group(1) is not None and content['return-path'] is not None: 1037 | 1038 | #print(f'{Fore.LIGHTGREEN_EX}Return-Path Field detected !{Fore.RESET}') 1039 | print(self.colors.green("Return-Path Field detected !")) 1040 | report.append('\nReturn-Path Field detected !') 1041 | 1042 | if formatReturnPath == False: 1043 | if fromMatch.group(1).strip().lower() != content['return-path'].strip().lower(): 1044 | #print(f'{Fore.LIGHTYELLOW_EX}{indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "RETURN-PATH" Field ({content["return-path"]}){Fore.RESET}') 1045 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "RETURN-PATH" Field ({content["return-path"]})')) 1046 | report.append(f'\n{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "RETURN-PATH" Field ({content["return-path"]})') 1047 | 1048 | else: 1049 | print(self.colors.green(f'{self.indent}→ No "FROM - RETURN-PATH" Mismatch detected.')) 1050 | report.append(f'\n{self.indent}→ No "FROM - RETURN-PATH" Mismatch detected.') 1051 | 1052 | elif formatReturnPath == True: 1053 | if fromMatch.group(1).strip().lower() != returnToPath.group(1).strip().lower(): 1054 | print(self.colors.yellow(f'{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "RETURN-PATH" Field ({returnToPath.group(1)})')) 1055 | report.append(f'\n{self.indent}→ Suspicious activity detected: "FROM" Field ({fromMatch.group(1)}) NOT EQUAL "RETURN-PATH" Field ({returnToPath.group(1)})') 1056 | 1057 | else: 1058 | print(self.colors.green(f'{self.indent}→ No "FROM - RETURN-PATH" Mismatch detected.')) 1059 | report.append(f'\n{self.indent}→ No "FROM - RETURN-PATH" Mismatch detected.') 1060 | 1061 | else: 1062 | print(self.colors.white("No Return-Path Field detected. Skipping...")) 1063 | report.append('No Return-Path Field detected. Skipping...') 1064 | 1065 | #------------------------Check with VirusTotal------------------------# 1066 | 1067 | #print(f'\n{Fore.LIGHTYELLOW_EX}Note: You can use your own VirusTotal, AbuseIPDB and IPQualityScore API Key to generate a report for the IP Address. Check the Source Code.{Fore.RESET}') 1068 | print(self.colors.yellow("\nNote: You can use your own VirusTotal, AbuseIPDB and IPQualityScore API Key to generate a report for the IP Address. Check the Source Code.")) 1069 | 1070 | print(self.colors.magenta("Checking with VirusTotal...")) 1071 | report.append('\n\nChecking with VirusTotal...\n') 1072 | 1073 | if filteredIpv4: 1074 | # If you got an VT API Key, you can use it here. It will generate a report for the IP Address. Uncomment the line under this comment and replace with your API Key. 1075 | #os.system(f'curl -s -X GET --header "x-apikey: " "https://www.virustotal.com/api/v3/ip_addresses/{ipv4[0]}" > vt.json') 1076 | 1077 | print(f'Detections: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/detection")}') 1078 | print(f'Relations: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/relations")}') 1079 | print(f'Graph: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/graph")}') 1080 | print(f'Network Traffic: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/network-traffic")}') 1081 | print(f'WHOIS: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/whois")}') 1082 | print(f'Comments: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/comments")}') 1083 | print(f'Votes: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/votes")}') 1084 | 1085 | report.append(f'Detections: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/detection\n') 1086 | report.append(f'Relations: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/relations\n') 1087 | report.append(f'Graph: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/graph\n') 1088 | report.append(f'Network Traffic: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/network-traffic\n') 1089 | report.append(f'WHOIS: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/whois\n') 1090 | report.append(f'Comments: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/comments\n') 1091 | report.append(f'Votes: https://www.virustotal.com/gui/ip-address/{filteredIpv4[0]}/votes\n') 1092 | 1093 | 1094 | elif authResultOrigIP: 1095 | print(f'Detections: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/detection")}'), 1096 | print(f'Relations: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/relations")}') 1097 | print(f'Graph: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/graph")}') 1098 | print(f'Network Traffic: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/network-traffic")}') 1099 | print(f'WHOIS: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/whois")}') 1100 | print(f'Comments: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/comments")}') 1101 | print(f'Votes: {self.colors.green(f"https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/votes")}') 1102 | 1103 | report.append(f'Detections: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/detection\n') 1104 | report.append(f'Relations: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/relations\n') 1105 | report.append(f'Graph: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/graph\n') 1106 | report.append(f'Network Traffic: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/network-traffic\n') 1107 | report.append(f'WHOIS: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/whois\n') 1108 | report.append(f'Comments: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/comments\n') 1109 | report.append(f'Votes: https://www.virustotal.com/gui/ip-address/{authResultOrigIP[0]}/votes\n') 1110 | 1111 | 1112 | 1113 | else: 1114 | #print(f'{Fore.WHITE}Could not detect SMTP Server. Manual reviewing required.{Fore.RESET}') 1115 | print(self.colors.white("Could not detect SMTP Server. Manual reviewing required.")) 1116 | report.append(f'Could not detect SMTP Server. Manual reviewing required.') 1117 | 1118 | #------------------------Check with AbuseIPDB------------------------# 1119 | 1120 | print(self.colors.magenta("\nChecking with AbuseIPDB...")) 1121 | report.append('\nChecking with AbuseIPDB...\n') 1122 | 1123 | if filteredIpv4: 1124 | # If you got an AbuseIPDB API Key, you can use it here. It will generate a report for the IP Address. Uncomment the line under this comment and replace with your API Key. 1125 | #os.system(f'curl -s -X GET --header "Key: " "https://api.abuseipdb.com/api/v2/check?ipAddress={ipv4[0]}" > abuseipdb.json') 1126 | print(f'AbuseIPDB: {self.colors.green(f"https://www.abuseipdb.com/check/{filteredIpv4[0]}")}') 1127 | report.append(f'AbuseIPDB: https://www.abuseipdb.com/check/{filteredIpv4[0]}') 1128 | 1129 | elif authResultOrigIP: 1130 | print(f'AbuseIPDB: {self.colors.green(f"https://www.abuseipdb.com/check/{authResultOrigIP[0]}")}') 1131 | report.append(f'AbuseIPDB: https://www.abuseipdb.com/check/{authResultOrigIP[0]}') 1132 | 1133 | else: 1134 | print(self.colors.white("Could not detect SMTP Server. Manual reviewing required.")) 1135 | report.append(f'Could not detect SMTP Server. Manual reviewing required.') 1136 | 1137 | #------------------------Check with IPQualityScore------------------------# 1138 | 1139 | #print(f'\n{Fore.LIGHTMAGENTA_EX}Checking with IPQualityScore...{Fore.RESET}') 1140 | print(self.colors.magenta("\nChecking with IPQualityScore...")) 1141 | report.append('\n\nChecking with IPQualityScore...\n') 1142 | 1143 | if filteredIpv4: 1144 | # If you got an IPQualityScore API Key, you can use it here. It will generate a report for the IP Address. Uncomment the line under this comment and replace with your API Key. 1145 | #os.system(f'curl -s -X GET --header "Key: " "https://www.ipqualityscore.com/api/json/ip//{ipv4[0]}" > ipqualityscore.json') 1146 | #print(f'IPQualityScore: {Fore.LIGHTGREEN_EX}https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{filteredIpv4[0]}{Fore.RESET}') 1147 | print(f'IPQualityScore: {self.colors.green(f"https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{filteredIpv4[0]}")}') 1148 | report.append(f'IPQualityScore: https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{filteredIpv4[0]}') 1149 | 1150 | elif authResultOrigIP: 1151 | #print(f'IPQualityScore: {Fore.LIGHTGREEN_EX}https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{authResultOrigIP[0]}{Fore.RESET}') 1152 | print(f'IPQualityScore: {self.colors.green(f"https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{authResultOrigIP[0]}")}') 1153 | report.append(f'IPQualityScore: https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/{authResultOrigIP[0]}') 1154 | 1155 | else: 1156 | #print(f'{Fore.WHITE}Could not detect SMTP Server. Manual reviewing required.{Fore.RESET}') 1157 | print(self.colors.white("Could not detect SMTP Server. Manual reviewing required.")) 1158 | report.append(f'Could not detect SMTP Server. Manual reviewing required.') 1159 | 1160 | return ''.join(report) -------------------------------------------------------------------------------- /core/utils.py: -------------------------------------------------------------------------------- 1 | from core.colors import Colors 2 | from email.parser import BytesParser 3 | from email.header import decode_header 4 | import dns.resolver 5 | import re 6 | from datetime import datetime 7 | import sys 8 | import hashlib 9 | 10 | 11 | class Utils: 12 | def __init__(self, header): 13 | self.eHeader = header 14 | self.colors = Colors() 15 | 16 | def getFields(self): 17 | fields = [] 18 | # First find all the fields present in the email headers 19 | with open(self.eHeader, "rb") as fp: 20 | headers = BytesParser().parse(fp) 21 | 22 | # Add each field to a list 23 | for j in headers: 24 | fields.append(j + ":") 25 | 26 | return fields 27 | 28 | 29 | def getReceivedFields(self): 30 | # credits goes to spcnvdr for helping me with this part of the code. https://github.com/spcnvdr/tracemail/tree/master Copyright 2020 spcnvdr 31 | 32 | #------------------------get all the "Receveid: from" Fields------------------------# 33 | 34 | found = False 35 | tmp = '' 36 | receivedFields =[] 37 | finalReceivedFields = [] 38 | fields = self.getFields() 39 | 40 | with open(self.eHeader, 'r', encoding='UTF-8') as header: 41 | for lines in header: 42 | separator = lines.split() 43 | 44 | if len(separator) != 0 and separator[0] in fields and found: 45 | receivedFields.append(tmp) 46 | tmp ='' 47 | if separator[0] != 'Received:': 48 | found = False 49 | else: 50 | tmp += lines 51 | elif found: 52 | tmp += lines 53 | elif 'Received:' in lines.split(): 54 | found = True 55 | tmp += lines 56 | 57 | for x in receivedFields: 58 | finalReceivedFields.append(' '.join(x.split())) 59 | 60 | return finalReceivedFields 61 | 62 | def resolveIP(self, domain): 63 | try: 64 | resolve4 = dns.resolver.resolve(domain, 'A') 65 | #resolve6 = dns.resolver.resolve(domain, 'AAAA') 66 | if resolve4: 67 | for resolved4 in resolve4: 68 | return f'{resolved4}' 69 | except: 70 | return f'{self.colors.red("Error.")}' 71 | 72 | 73 | def routing(self): 74 | 75 | routing =[] 76 | counter= 0 # counter for the hops 77 | 78 | print(f'\n{self.colors.light_blue("Relay Routing: ")}') 79 | routing.append(f'Relay Routing:\n') 80 | 81 | for y in reversed(self.getReceivedFields()): 82 | 83 | # Regex for the field values 84 | receivedMatch = re.findall(r'received: from ([\w\-.:]+)', y, re.IGNORECASE) 85 | byMatch = re.findall(r'by ([\w\-.:]+)', y, re.IGNORECASE) 86 | withMatch = re.findall(r'with ([\w\-.:]+)', y, re.IGNORECASE) 87 | 88 | counter += 1 89 | try: 90 | if len(receivedMatch) != 0: 91 | print(f'Hop {counter} |↓|: FROM {self.colors.green(receivedMatch[0].lower())} TO {self.colors.green(byMatch[0].lower())} WITH {self.colors.cyan(withMatch[0].lower())}') 92 | routing.append(f'Hop {counter} |↓|: FROM {receivedMatch[0].lower()} TO {byMatch[0].lower()} WITH {withMatch[0].lower()}\n') 93 | else: 94 | print(f'{self.colors.yellow("No match found for Hop " + str(counter))}') 95 | except Exception as e: 96 | print(f'{self.colors.red("Error: " + str(e) + ". Skipping...")}') 97 | 98 | print(f'\n{self.colors.light_blue("Timestamps between Hops: ")}') 99 | routing.append(f'\nTimestamps between Hops:\n') 100 | 101 | dateCounter = 1 #separate counter for the hops in the timestamps 102 | prevTimestamp = None 103 | delta = None 104 | 105 | for x in reversed(self.getReceivedFields()): 106 | # There are potentialy 3 different date formats in the received fields. 107 | # That's why we have to check for each one of them. 108 | dateMatch1 = re.findall(r'\S{3},[ ]{0,4} \d{1,2} \S{3} \d{1,4} \d{2}:\d{2}:\d{2} [+-]\d{4}', x ,re.IGNORECASE) 109 | dateMatch2 = re.findall(r'\S{3}, \d{2} \S{3} \d{1,4} \d{2}:\d{2}:\d{2}.\d{0,10} [+-]\d{4}', x, re.IGNORECASE) 110 | dateMatch3 = re.findall(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{0,10} [+-]\d{4}', x, re.IGNORECASE) 111 | 112 | if dateMatch1 is not None: 113 | for date in reversed(dateMatch1): 114 | currentTimeStamp = datetime.strptime(date, '%a, %d %b %Y %H:%M:%S %z') 115 | if prevTimestamp: 116 | delta = currentTimeStamp - prevTimestamp 117 | print(f'Hop {dateCounter}: {self.colors.green(date)}, {self.colors.cyan("Delta: " + str(delta))}') 118 | routing.append(f'Hop {dateCounter}: {date}, Delta: {delta if delta is not None else "N/A"}\n') 119 | dateCounter += 1 120 | prevTimestamp = currentTimeStamp 121 | 122 | elif dateMatch2 is not None: 123 | for date in reversed(dateMatch2): 124 | currentTimeStamp = datetime.strptime(date, '%a, %d %b %Y %H:%M:%S.%f %z') 125 | if prevTimestamp: 126 | delta = currentTimeStamp - prevTimestamp 127 | print(f'Hop {dateCounter}: {self.colors.green(date)}, {self.colors.cyan("Delta: " + str(delta))}') 128 | routing.append(f'Hop {dateCounter}: {date}, Delta: {delta if delta is not None else "N/A"}\n') 129 | dateCounter += 1 130 | prevTimestamp = currentTimeStamp 131 | 132 | elif dateMatch3 is not None: 133 | for date in reversed(dateMatch3): 134 | currentTimeStamp = datetime.strptime(date, '%Y-%m-%d %H:%M:%S.%f %z') 135 | if prevTimestamp: 136 | delta = currentTimeStamp - prevTimestamp 137 | print(f'Hop {dateCounter}: {self.colors.green(date)}, {self.colors.cyan("Delta: " + str(delta))}') 138 | routing.append(f'Hop {dateCounter}: {date}\n, Delta: {delta if delta is not None else "N/A"}\n') 139 | dateCounter += 1 140 | prevTimestamp = currentTimeStamp 141 | 142 | return ''.join(routing) 143 | 144 | 145 | def generalInformation(self): 146 | try: 147 | with open(self.eHeader, 'r', encoding='UTF-8') as header: 148 | raw_content = header.read() 149 | content = BytesParser().parsebytes(raw_content.encode('UTF-8')) 150 | except FileNotFoundError: 151 | print(f'{self.colors.red("File not found.")}') 152 | sys.exit(1) 153 | except Exception as e: 154 | print(f'{self.colors.red(f"Error reading file: {e}")}') 155 | sys.exit(1) 156 | 157 | # Trying to decode the subject 158 | subject = content.get('subject', 'No subject found') 159 | if subject != 'No subject found': 160 | decoded_subject = decode_header(subject) 161 | decodedHeader = '' 162 | for part, charset in decoded_subject: 163 | try: 164 | decodedHeader += part.decode(charset or 'utf8') if isinstance(part, bytes) else part 165 | except UnicodeDecodeError: 166 | decodedHeader += part.decode('iso-8859-1') if isinstance(part, bytes) else part 167 | else: 168 | decodedHeader = subject 169 | 170 | print(f'\n{self.colors.light_blue("General Information:")}') 171 | 172 | from_field = content.get("from", "No 'From' field found") 173 | to_field = content.get("to", "No 'To' field found") 174 | date_field = content.get("date", "No 'Date' field found") 175 | 176 | print(f'From: {self.colors.green(from_field)}') 177 | print(f'To: {self.colors.green(to_field)}') 178 | print(f'Subject: {self.colors.green(decodedHeader)}') 179 | print(f'Date: {self.colors.green(date_field)}') 180 | 181 | return f'From: {from_field}\nTo: {to_field}\nSubject: {decodedHeader}\nDate: {date_field}' 182 | 183 | 184 | def securityInformations(self): 185 | try: 186 | with open(self.eHeader, 'r', encoding='UTF-8') as header: 187 | content = BytesParser().parsebytes(header.read().encode('UTF-8')) 188 | except FileNotFoundError: 189 | self.colors.red("File not found.") 190 | sys.exit(1) 191 | 192 | secInfos = [] 193 | 194 | print(f'\n{self.colors.light_blue("Security Informations: ")}') 195 | secInfos.append(f'\nSecurity Informations:\n') 196 | 197 | if content['received-spf'] is not None: 198 | if 'fail' in content['received-spf'].lower(): 199 | print(f'Received SPF: {self.colors.red(content["received-spf"])}') 200 | secInfos.append(f'Received SPF: {content["received-spf"]}') 201 | 202 | elif 'None' in content['received-spf']: 203 | print(f'Received SPF: {self.colors.red(content["received-spf"])}') 204 | secInfos.append(f'Received SPF: {content["received-spf"]}') 205 | 206 | else: 207 | print(f'Received SPF: {self.colors.green(content["received-spf"])}') 208 | secInfos.append(f'Received SPF: {content["received-spf"]}') 209 | else: 210 | print(f'Received SPF: {self.colors.red("No Received SPF")}') 211 | secInfos.append(f'Received SPF: No Received SPF') 212 | 213 | 214 | if content['dkim-signature'] is not None: 215 | print(f'DKIM Signature: {self.colors.green(content["dkim-signature"])}') 216 | secInfos.append(f'DKIM Signature: {content["dkim-signature"]}') 217 | else: 218 | print(f'DKIM Signature: {self.colors.red("No DKIM Signature")}') 219 | secInfos.append(f'DKIM Signature: No DKIM Signature') 220 | 221 | 222 | if content['dmarc'] is not None: 223 | print(f'DMARC: {self.colors.green(content["dmarc"])}') 224 | secInfos.append(f'DMARC: {content["dmarc"]}') 225 | else: 226 | print(f'DMARC: {self.colors.red("No DMARC")}') 227 | secInfos.append(f'DMARC: No DMARC') 228 | 229 | 230 | if content['authentication-results'] is not None: 231 | 232 | if 'spf=fail' in content['authentication-results'].lower(): 233 | print(f'Authentication Results: {self.colors.red(content["authentication-results"])}') 234 | secInfos.append(f'Authentication Results: {content["authentication-results"]}') 235 | else: 236 | print(f'Authentication Results: {self.colors.green(content["authentication-results"])}') 237 | secInfos.append(f'Authentication Results: {content["authentication-results"]}') 238 | 239 | else: 240 | print(f'Authentication Results: {self.colors.red("No Authentication Results")}') 241 | secInfos.append(f'Authentication Results: No Authentication Results') 242 | 243 | 244 | if content['x-forefront-antispam-report'] is not None: 245 | print(f'X-Forefront-Antispam-Report: {self.colors.green(content["x-forefront-antispam-report"])}') 246 | secInfos.append(f'X-Forefront-Antispam-Report: {content["x-forefront-antispam-report"]}') 247 | else: 248 | print(f'X-Forefront-Antispam-Report: {self.colors.red("No X-Forefront-Antispam-Report")}') 249 | secInfos.append(f'X-Forefront-Antispam-Report: No X-Forefront-Antispam-Report') 250 | 251 | 252 | if content['x-microsoft-antispam'] is not None: 253 | print(f'X-Microsoft-Antispam: {self.colors.green(content["x-microsoft-antispam"])}') 254 | secInfos.append(f'X-Microsoft-Antispam: {content["x-microsoft-antispam"]}') 255 | else: 256 | print(f'X-Microsoft-Antispam: {self.colors.red("No X-Microsoft-Antispam")}') 257 | secInfos.append(f'X-Microsoft-Antispam: No X-Microsoft-Antispam') 258 | 259 | return '\n'.join(secInfos) 260 | 261 | def envelope(self): 262 | try: 263 | with open(self.eHeader, 'r', encoding='UTF-8') as header: 264 | content = BytesParser().parsebytes(header.read().encode('UTF-8')) 265 | except FileNotFoundError: 266 | self.colors.red("File not found.") 267 | sys.exit(1) 268 | 269 | eenvelope = [] 270 | 271 | print(f'\n{self.colors.light_blue("Other Interesting Headers: ")}') 272 | eenvelope.append(f'\nOther Interesting Headers:\n') 273 | 274 | 275 | if content['X-ORIG-EnvelopeFrom'] is not None: 276 | fromMatch = re.search(r'<(.*)>', content['from']) 277 | 278 | if content['X-ORIG-EnvelopeFrom'] == 'anonymous@': 279 | print(f'Envelope From: {self.colors.red(content["X-ORIG-EnvelopeFrom"])}') 280 | eenvelope.append(f'Envelope From: {content["X-ORIG-EnvelopeFrom"]}') 281 | 282 | elif content['X-ORIG-EnvelopeFrom'] != fromMatch.group(1): 283 | from_address = content['from'] 284 | orig_envelope_from = content['X-ORIG-EnvelopeFrom'] 285 | message = f"POTENTIAL SPOOFING ATTACK DETECTED: FROM ({from_address}) NOT EQUAL ({orig_envelope_from})" 286 | print(f'Envelope From: {self.colors.red(message)}') 287 | eenvelope.append(message) 288 | 289 | else: 290 | print(f'Envelope From: {self.colors.green(content["X-ORIG-EnvelopeFrom"])}') 291 | eenvelope.append(f'Envelope From: {content["X-ORIG-EnvelopeFrom"]}') 292 | else: 293 | print(f'Envelope From: {self.colors.red("No Envelope From")}') 294 | eenvelope.append(f'Envelope From: No Envelope From') 295 | 296 | 297 | if content['return-path'] is not None: 298 | print(f'Return Path: {self.colors.green(content["return-path"])}') 299 | eenvelope.append(f'Return Path: {content["return-path"]}') 300 | else: 301 | print(f'Return Path: {self.colors.red("No Return Path")}') 302 | eenvelope.append(f'Return Path: No Return Path') 303 | 304 | if content['message-id'] is not None: 305 | print(f'Message ID: {self.colors.green(content["message-id"])}') 306 | eenvelope.append(f'Message ID: {content["message-id"]}') 307 | else: 308 | print(f'Message ID: {self.colors.red("No Message ID")}') 309 | eenvelope.append(f'Message ID: No Message ID') 310 | 311 | if content['mime-version'] is not None: 312 | print(f'MIME-Version: {self.colors.green(content["mime-version"])}') 313 | eenvelope.append(f'MIME-Version: {content["mime-version"]}') 314 | else: 315 | print(f'MIME-Version: {self.colors.red("No MIME-Version")}') 316 | eenvelope.append(f'MIME-Version: No MIME-Version') 317 | 318 | if content['authentication-results-original'] is not None: 319 | if 'spf=fail' in content['authentication-results-original'].lower(): 320 | print(f'Authentication-Results-Original: {self.colors.red(content["authentication-results-original"])}') 321 | eenvelope.append(f'Authentication-Results-Original: {content["authentication-results-original"]}') 322 | 323 | elif 'spf=pass' in content['authentication-results-original'].lower(): 324 | print(f'Authentication-Results-Original: {self.colors.green(content["authentication-results-original"])}') 325 | eenvelope.append(f'Authentication-Results-Original: {content["authentication-results-original"]}') 326 | 327 | else: 328 | print(f'Authentication-Results-Original: {self.colors.yellow(content["authentication-results-original"])}') 329 | eenvelope.append(f'Authentication-Results-Original: {content["authentication-results-original"]}') 330 | else: 331 | print(f'Authentication-Results-Original: {self.colors.red("No Authentication-Results-Original")}') 332 | eenvelope.append(f'Authentication-Results-Original: No Authentication-Results-Original') 333 | 334 | message = '\n<---------MS Exchange Organization Headers--------->\n' 335 | print(f'{self.colors.cyan(message)}') 336 | eenvelope.append(message) 337 | 338 | if content['x-ms-exchange-organization-authas'] is not None: 339 | if 'anonymous' or 'Anonymous' in content['x-ms-exchange-organization-authas']: 340 | print(f'X-MS-Exchange-Organization-AuthAs: {self.colors.yellow(content["x-ms-exchange-organization-authas"])}') 341 | eenvelope.append(f'X-MS-Exchange-Organization-AuthAs: {content["x-ms-exchange-organization-authas"]}') 342 | else: 343 | print(f'X-MS-Exchange-Organization-AuthAs: {self.colors.green(content["x-ms-exchange-organization-authas"])}') 344 | eenvelope.append(f'X-MS-Exchange-Organization-AuthAs: {content["x-ms-exchange-organization-authas"]}') 345 | else: 346 | print(f'X-MS-Exchange-Organization-AuthAs: {self.colors.red("No X-MS-Exchange-Organization-AuthAs")}') 347 | eenvelope.append(f'X-MS-Exchange-Organization-AuthAs: No X-MS-Exchange-Organization-AuthAs') 348 | 349 | if content['x-ms-exchange-organization-authsource'] is not None: 350 | print(f'X-MS-Exchange-Organization-AuthSource: {self.colors.green(content["x-ms-exchange-organization-authsource"])}') 351 | eenvelope.append(f'X-MS-Exchange-Organization-AuthSource: {content["x-ms-exchange-organization-authsource"]}') 352 | else: 353 | print(f'X-MS-Exchange-Organization-AuthSource: {self.colors.red("No X-MS-Exchange-Organization-AuthSource")}') 354 | eenvelope.append(f'X-MS-Exchange-Organization-AuthSource: No X-MS-Exchange-Organization-AuthSource') 355 | 356 | if content['x-ms-exchange-organization-authmechanism'] is not None: 357 | print(f'X-MS-Exchange-Organization-AuthMechanism: {self.colors.green(content["x-ms-exchange-organization-authmechanism"])}') 358 | eenvelope.append(f'X-MS-Exchange-Organization-AuthMechanism: {content["x-ms-exchange-organization-authmechanism"]}') 359 | else: 360 | print(f'X-MS-Exchange-Organization-AuthMechanism: {self.colors.red("No X-MS-Exchange-Organization-AuthMechanism")}') 361 | eenvelope.append(f'X-MS-Exchange-Organization-AuthMechanism: No X-MS-Exchange-Organization-AuthMechanism') 362 | 363 | if content['x-ms-exchange-organization-network-message-id'] is not None: 364 | print(f'X-MS-Exchange-Organization-Network-Message-Id: {self.colors.green(content["x-ms-exchange-organization-network-message-id"])}') 365 | eenvelope.append(f'X-MS-Exchange-Organization-Network-Message-Id: {content["x-ms-exchange-organization-network-message-id"]}') 366 | else: 367 | #print(f'X-MS-Exchange-Organization-Network-Message-Id: {Fore.LIGHTRED_EX}No X-MS-Exchange-Organization-Network-Message-Id{Fore.RESET}') 368 | print(f'X-MS-Exchange-Organization-Network-Message-Id: {self.colors.red("No X-MS-Exchange-Organization-Network-Message-Id")}') 369 | eenvelope.append(f'X-MS-Exchange-Organization-Network-Message-Id: No X-MS-Exchange-Organization-Network-Message-Id') 370 | 371 | message = '\n<-------------------------------------------------->\n' 372 | print(f'{self.colors.cyan(message)}') 373 | eenvelope.append(message) 374 | 375 | return '\n'.join(eenvelope) 376 | 377 | def check_attachment(self, attachment): 378 | result = [] 379 | indent = " " 380 | 381 | #print(f'\n\n{Fore.LIGHTBLUE_EX}Checking the attachment...{Fore.RESET}') 382 | print(Colors.light_blue("\nChecking the attachment...")) 383 | result.append('\n\nChecking the attachment...\n') 384 | 385 | sha256 = hashlib.sha256() 386 | BUFFER = 65536 387 | 388 | with open(attachment, 'rb') as file: 389 | while True: 390 | data = file.read(BUFFER) 391 | if not data: 392 | break 393 | 394 | sha256.update(data) 395 | 396 | #print(f'{indent}--> Link: {Fore.LIGHTGREEN_EX}https://www.virustotal.com/gui/file/{sha256.hexdigest()}/detection{Fore.RESET}') 397 | print(f'{indent}--> Link: {Colors.green(f"https://www.virustotal.com/gui/file/{sha256.hexdigest()}/detection")}') 398 | result.append(f'--> Link: https://www.virustotal.com/gui/file/{sha256.hexdigest()}/detection') 399 | 400 | return '\n'.join(result) 401 | 402 | 403 | -------------------------------------------------------------------------------- /dist/elyzer.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mochabyte0x/Elyzer/132da05e1830aac57ac7f5f1266c3d725a6c05c2/dist/elyzer.exe -------------------------------------------------------------------------------- /elyzer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import re 4 | import json 5 | import requests 6 | 7 | from core.utils import Utils 8 | from core.spoofing import Spoofing 9 | from argparse import ArgumentParser 10 | from core.colors import Colors, banner 11 | from core.exp_json import ExportJson, export_to_json 12 | 13 | 14 | def checkForUpdates(): 15 | try: 16 | response = requests.get('https://api.github.com/repos/B0lg0r0v/Elyzer/releases/latest') 17 | except requests.exceptions.ConnectionError: 18 | #print(f'{Fore.RED}No internet connection.{Fore.RESET}') 19 | print(Colors.red("No internet connection.")) 20 | exit() 21 | 22 | latestRelease = json.loads(response.text) 23 | 24 | if 'tag_name' in latestRelease: 25 | latestVersion = latestRelease['tag_name'].lower() 26 | 27 | match = re.search(r'v\d+\.\d\.\d+', latestVersion) #Extract only the version number 28 | if match: 29 | latestVersion = match.group(0) 30 | 31 | if CURRENT_VERSION != latestVersion: 32 | if latestVersion > CURRENT_VERSION: 33 | print(f'A new version ({latestVersion}) is available. Please download it from the release section on GitHub.\n') 34 | elif latestVersion == CURRENT_VERSION: 35 | pass 36 | elif latestVersion < CURRENT_VERSION: 37 | pass 38 | 39 | 40 | if __name__ == '__main__': 41 | 42 | indent = ' ' * 3 43 | CURRENT_VERSION = 'v0.5.0' 44 | savings = [] 45 | 46 | checkForUpdates() 47 | 48 | parser = ArgumentParser() # Create the Parser. 49 | parser.add_argument('-f', '--file', help='Give the E-Mail Header as a file.', required=True) 50 | parser.add_argument('-pa', '--passive', help='Enables the passive mode. DNS resolution is performed passively through Driftnet for better OPSEC. You need to add "DRIFTNET_API" as an environment variable in order to use this feature.', action='store_true') 51 | parser.add_argument('-nd', '--no-dns', help='Enables the no-dns mode. No DNS resolution is performed for best OPSEC. This heavily affects the results !', action='store_true') 52 | parser.add_argument('-q', '--quiet', help='Quiet mode. Disables banner.', action='store_true') 53 | parser.add_argument('-j', '--json', help='EXPERIMENTAL FEATURE. Output the results in JSON format.', action='store_true') 54 | parser.add_argument('-v', '--version', action='version', version=f'Elyzer {CURRENT_VERSION}') 55 | parser.add_argument('-a', '--attachement', help='Check if the file is malicious.') 56 | args = parser.parse_args() # Initialize the Parser. 57 | 58 | 59 | if args.file is not None: 60 | utils = Utils(args.file) # Initialize the Utils class. 61 | spoofing = Spoofing(args.file) # Initialize the Spoofing class. 62 | expjson = ExportJson(args.file) 63 | current_dir = os.path.dirname(os.path.abspath(__file__)) 64 | 65 | if args.quiet: 66 | 67 | print(Colors.yellow("E-Mail Header Analysis complete")) 68 | if args.passive: 69 | 70 | if args.json: 71 | if args.attachement: 72 | utils.generalInformation() 73 | utils.routing() 74 | utils.securityInformations() 75 | utils.envelope() 76 | spoofing.spoofing_passive_dns() 77 | utils.check_attachment(args.attachement) 78 | 79 | json_output = expjson.jsonoutput() 80 | json_file = export_to_json(json_output, os.path.join(current_dir, "elyzer_report.json")) 81 | print(f'\nElyzer.json has been saved to {json_file}') 82 | 83 | else: 84 | utils.generalInformation() 85 | utils.routing() 86 | utils.securityInformations() 87 | utils.envelope() 88 | spoofing.spoofing_passive_dns() 89 | 90 | json_output = expjson.jsonoutput() 91 | json_file = export_to_json(json_output, os.path.join(current_dir, "elyzer_report.json")) 92 | print(f'\nElyzer.json has been saved to {json_file}') 93 | 94 | else: 95 | if args.attachement: 96 | utils.generalInformation() 97 | utils.routing() 98 | utils.securityInformations() 99 | utils.envelope() 100 | spoofing.spoofing_passive_dns() 101 | utils.check_attachment(args.attachement) 102 | 103 | else: 104 | utils.generalInformation() 105 | utils.routing() 106 | utils.securityInformations() 107 | utils.envelope() 108 | spoofing.spoofing_passive_dns() 109 | 110 | elif args.no_dns: 111 | 112 | if args.json: 113 | 114 | if args.attachement: 115 | utils.generalInformation() 116 | utils.routing() 117 | utils.securityInformations() 118 | utils.envelope() 119 | spoofing.spoofing_no_dns() 120 | utils.check_attachment(args.attachement) 121 | 122 | json_output = expjson.jsonoutput() 123 | json_file = export_to_json(json_output, os.path.join(current_dir, "elyzer_report.json")) 124 | print(f'\nElyzer.json has been saved to {json_file}') 125 | 126 | else: 127 | utils.generalInformation() 128 | utils.routing() 129 | utils.securityInformations() 130 | utils.envelope() 131 | spoofing.spoofing_no_dns() 132 | 133 | json_output = expjson.jsonoutput() 134 | json_file = export_to_json(json_output, os.path.join(current_dir, "elyzer_report.json")) 135 | print(f'\nElyzer.json has been saved to {json_file}') 136 | 137 | else: 138 | if args.attachement: 139 | utils.generalInformation() 140 | utils.routing() 141 | utils.securityInformations() 142 | utils.envelope() 143 | spoofing.spoofing_no_dns() 144 | utils.check_attachment(args.attachement) 145 | 146 | else: 147 | utils.generalInformation() 148 | utils.routing() 149 | utils.securityInformations() 150 | utils.envelope() 151 | spoofing.spoofing_no_dns() 152 | 153 | else: 154 | 155 | if args.json: 156 | if args.attachement: 157 | utils.generalInformation() 158 | utils.routing() 159 | utils.securityInformations() 160 | utils.envelope() 161 | spoofing.spoofing_all_checks() 162 | utils.check_attachment(args.attachement) 163 | 164 | json_output = expjson.jsonoutput() 165 | json_file = export_to_json(json_output, os.path.join(current_dir, "elyzer_report.json")) 166 | print(f'\nElyzer.json has been saved to {json_file}') 167 | 168 | else: 169 | utils.generalInformation() 170 | utils.routing() 171 | utils.securityInformations() 172 | utils.envelope() 173 | spoofing.spoofing_all_checks() 174 | 175 | json_output = expjson.jsonoutput() 176 | json_file = export_to_json(json_output, os.path.join(current_dir, "elyzer_report.json")) 177 | print(f'\nElyzer.json has been saved to {json_file}') 178 | 179 | else: 180 | if args.attachement: 181 | utils.generalInformation() 182 | utils.routing() 183 | utils.securityInformations() 184 | utils.envelope() 185 | spoofing.spoofing_all_checks() 186 | utils.check_attachment(args.attachement) 187 | 188 | else: 189 | utils.generalInformation() 190 | utils.routing() 191 | utils.securityInformations() 192 | utils.envelope() 193 | spoofing.spoofing_all_checks() 194 | 195 | else: 196 | banner() 197 | print(Colors.yellow("E-Mail Header Analysis complete")) 198 | if args.passive: 199 | 200 | if args.attachement: 201 | utils.generalInformation() 202 | utils.routing() 203 | utils.securityInformations() 204 | utils.envelope() 205 | spoofing.spoofing_passive_dns() 206 | utils.check_attachment(args.attachement) 207 | 208 | else: 209 | utils.generalInformation() 210 | utils.routing() 211 | utils.securityInformations() 212 | utils.envelope() 213 | spoofing.spoofing_passive_dns() 214 | 215 | elif args.no_dns: 216 | 217 | if args.attachement: 218 | utils.generalInformation() 219 | utils.routing() 220 | utils.securityInformations() 221 | utils.envelope() 222 | spoofing.spoofing_no_dns() 223 | utils.check_attachment(args.attachement) 224 | 225 | else: 226 | utils.generalInformation() 227 | utils.routing() 228 | utils.securityInformations() 229 | utils.envelope() 230 | spoofing.spoofing_no_dns() 231 | 232 | else: 233 | 234 | if args.json: 235 | 236 | if args.attachement: 237 | utils.generalInformation() 238 | utils.routing() 239 | utils.securityInformations() 240 | utils.envelope() 241 | spoofing.spoofing_all_checks() 242 | utils.check_attachment(args.attachement) 243 | 244 | json_output = expjson.jsonoutput() 245 | json_file = export_to_json(json_output) 246 | print(f'\nElyzer.json has been saved to {json_file}') 247 | 248 | else: 249 | utils.generalInformation() 250 | utils.routing() 251 | utils.securityInformations() 252 | utils.envelope() 253 | spoofing.spoofing_all_checks() 254 | 255 | json_output = expjson.jsonoutput() 256 | json_file = export_to_json(json_output, os.path.join(current_dir, "elyzer_report.json")) 257 | print(f'\nElyzer.json has been saved to {json_file}') 258 | 259 | else: 260 | if args.attachement: 261 | utils.generalInformation() 262 | utils.routing() 263 | utils.securityInformations() 264 | utils.envelope() 265 | spoofing.spoofing_all_checks() 266 | utils.check_attachment(args.attachement) 267 | 268 | else: 269 | utils.generalInformation() 270 | utils.routing() 271 | utils.securityInformations() 272 | utils.envelope() 273 | spoofing.spoofing_all_checks() 274 | 275 | else: 276 | parser.error('E-Mail Header is required.') -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | colorama 2 | dnspython>=2.6.1 3 | Requests>=2.32.2 4 | --------------------------------------------------------------------------------