mirror of
https://github.com/MattKeeley/Spoofy.git
synced 2026-02-03 05:23:24 +00:00
Merge pull request #27 from MattKeeley/26-spoofing-not-possible-without-spf-or-dmarc-record
New improvements and test cases
This commit is contained in:
26
.github/workflows/ci.yml
vendored
26
.github/workflows/ci.yml
vendored
@@ -6,12 +6,20 @@ jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install dependencies
|
||||
run: pip install -r requirements.txt
|
||||
- name: Run tests
|
||||
run: python3 test.py
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
pip install ruff
|
||||
|
||||
- name: Run Ruff
|
||||
run: ruff check .
|
||||
|
||||
- name: Run tests
|
||||
run: python3 test.py
|
||||
|
||||
31
README.md
31
README.md
@@ -5,42 +5,56 @@
|
||||
Spoofy
|
||||
</h1>
|
||||
|
||||
|
||||
[](https://www.python.org/)
|
||||
[](https://www.thewholesomedish.com/spaghetti/)
|
||||
[](https://www.youtube.com/watch?v=kyti25ol438)
|
||||
|
||||
|
||||
## WHAT
|
||||
|
||||
`Spoofy` is a program that checks if a list of domains can be spoofed based on SPF and DMARC records. You may be asking, "Why do we need another tool that can check if a domain can be spoofed?"
|
||||
|
||||
Well, Spoofy is different and here is why:
|
||||
|
||||
> 1. Authoritative lookups on all lookups with known fallback (Cloudflare DNS)
|
||||
> 2. Accurate bulk lookups
|
||||
> 3. Custom, manually tested spoof logic (No guessing or speculating, real world test results)
|
||||
> 4. SPF lookup counter
|
||||
> 3. Custom, manually tested spoof logic (No guessing or speculating, real world test results)
|
||||
> 4. SPF DNS query counter
|
||||
|
||||
## PASSING TESTS
|
||||
|
||||
[](https://github.com/MattKeeley/Spoofy/actions/workflows/ci.yml)
|
||||
|
||||
## HOW TO USE
|
||||
|
||||
`Spoofy` requires **Python 3+**. Python 2 is not supported. Usage is shown below:
|
||||
|
||||
```console
|
||||
Usage:
|
||||
./spoofy.py -d [DOMAIN] -o [stdout or xls]
|
||||
./spoofy.py -d [DOMAIN] -o [stdout or xls] -t [NUMBER_OF_THREADS]
|
||||
OR
|
||||
./spoofy.py -iL [DOMAIN_LIST] -o [stdout or xls]
|
||||
|
||||
./spoofy.py -iL [DOMAIN_LIST] -o [stdout or xls] -t [NUMBER_OF_THREADS]
|
||||
|
||||
Options:
|
||||
-d : Process a single domain.
|
||||
-iL : Provide a file containing a list of domains to process.
|
||||
-o : Specify the output format: stdout (default) or xls.
|
||||
-t : Set the number of threads to use (default: 4).
|
||||
|
||||
Examples:
|
||||
./spoofy.py -d example.com -t 10
|
||||
./spoofy.py -iL domains.txt -o xls
|
||||
|
||||
Install Dependencies:
|
||||
pip3 install -r requirements.txt
|
||||
```
|
||||
|
||||
## HOW DO YOU KNOW ITS SPOOFABLE
|
||||
|
||||
(The spoofability table lists every combination of SPF and DMARC configurations that impact deliverability to the inbox, except for DKIM modifiers.)
|
||||
[Download Here](/files/Master_Table.xlsx)
|
||||
|
||||
## METHODOLOGY
|
||||
## METHODOLOGY
|
||||
|
||||
The creation of the spoofability table involved listing every relevant SPF and DMARC configuration, combining them, and then conducting SPF and DMARC information collection using an early version of Spoofy on a large number of US government domains. Testing if an SPF and DMARC combination was spoofable or not was done using the email security pentesting suite at [emailspooftest](https://emailspooftest.com/) using Microsoft 365. However, the initial testing was conducted using Protonmail and Gmail, but these services were found to utilize reverse lookup checks that affected the results, particularly for subdomain spoof testing. As a result, Microsoft 365 was used for the testing, as it offered greater control over the handling of mail.
|
||||
|
||||
After the initial testing using Microsoft 365, some combinations were retested using Protonmail and Gmail due to the differences in their handling of banners in emails. Protonmail and Gmail can place spoofed mail in the inbox with a banner or in spam without a banner, leading to some SPF and DMARC combinations being reported as "Mailbox Dependent" when using Spoofy. In contrast, Microsoft 365 places both conditions in spam. The testing and data collection process took several days to complete, after which a good master table was compiled and used as the basis for the Spoofy spoofability logic.
|
||||
@@ -63,7 +77,6 @@ Logo: cobracode
|
||||
|
||||
Tool was inspired by [Bishop Fox's](https://github.com/BishopFox/) project called [spoofcheck](https://github.com/BishopFox/spoofcheck/).
|
||||
|
||||
|
||||
## LICENSE
|
||||
|
||||
This project is licensed under the Creative Commons Zero v1.0 Universal - see the [LICENSE](LICENSE) file for details
|
||||
|
||||
46
libs/bimi.py
46
libs/bimi.py
@@ -1,46 +0,0 @@
|
||||
import dns.resolver
|
||||
|
||||
|
||||
def get_bimi_record(domain, dns_server):
|
||||
"""Returns the BIMI record for a given domain."""
|
||||
try:
|
||||
resolver = dns.resolver.Resolver()
|
||||
resolver.nameservers = [dns_server, '1.1.1.1', '8.8.8.8']
|
||||
query_result = resolver.resolve('default._bimi.' + domain, 'TXT')
|
||||
for record in query_result:
|
||||
if 'v=BIMI' in str(record):
|
||||
return record
|
||||
return None
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def get_bimi_details(bimi_record):
|
||||
"""Returns a tuple containing policy, pct, aspf, subdomain policy,
|
||||
forensic report uri, and aggregate report uri from a BIMI record"""
|
||||
version = get_bimi_version(bimi_record)
|
||||
location = get_bimi_location(bimi_record)
|
||||
authority = get_bimi_authority(bimi_record)
|
||||
return version, location, authority
|
||||
|
||||
|
||||
def get_bimi_version(bimi_record):
|
||||
"""Returns the version value from a BIMI record."""
|
||||
if "v=" in str(bimi_record):
|
||||
return str(bimi_record).split("v=")[1].split(";")[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_bimi_location(bimi_record):
|
||||
"""Returns the location value from a BIMI record."""
|
||||
if "l=" in str(bimi_record):
|
||||
return str(bimi_record).split("l=")[1].split(";")[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_bimi_authority(bimi_record):
|
||||
"""Returns the authority value from a BIMI record."""
|
||||
if "a=" in str(bimi_record):
|
||||
return str(bimi_record).split("a=")[1].split(";")[0]
|
||||
else:
|
||||
return None
|
||||
@@ -1,81 +0,0 @@
|
||||
import dns.resolver
|
||||
import tldextract
|
||||
|
||||
|
||||
def get_dmarc_record(domain, dns_server):
|
||||
"""Returns the DMARC record for a given domain."""
|
||||
subdomain = tldextract.extract(domain).registered_domain
|
||||
if subdomain != domain:
|
||||
return get_dmarc_record(subdomain, dns_server)
|
||||
try:
|
||||
resolver = dns.resolver.Resolver()
|
||||
resolver.nameservers = [dns_server]
|
||||
dmarc = resolver.resolve(f"_dmarc.{domain}", "TXT")
|
||||
except:
|
||||
return None
|
||||
|
||||
for dns_data in dmarc:
|
||||
if "DMARC1" in str(dns_data):
|
||||
dmarc_record = str(dns_data).replace('"', '')
|
||||
return dmarc_record
|
||||
return None
|
||||
|
||||
|
||||
def get_dmarc_details(dmarc_record):
|
||||
"""Returns a tuple containing policy, pct, aspf, subdomain policy,
|
||||
forensic report uri, and aggregate report uri from a DMARC record"""
|
||||
p = get_dmarc_policy(dmarc_record)
|
||||
pct = get_dmarc_pct(dmarc_record)
|
||||
aspf = get_dmarc_aspf(dmarc_record)
|
||||
sp = get_dmarc_subdomain_policy(dmarc_record)
|
||||
fo = get_dmarc_forensic_reports(dmarc_record)
|
||||
rua = get_dmarc_aggregate_reports(dmarc_record)
|
||||
return p, pct, aspf, sp, fo, rua
|
||||
|
||||
|
||||
def get_dmarc_policy(dmarc_record):
|
||||
"""Returns the policy value from a DMARC record."""
|
||||
if "p=" in str(dmarc_record):
|
||||
return str(dmarc_record).split("p=")[1].split(";")[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_dmarc_pct(dmarc_record):
|
||||
"""Returns the pct value from a DMARC record."""
|
||||
if "pct=" in str(dmarc_record):
|
||||
return str(dmarc_record).split("pct=")[1].split(";")[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_dmarc_aspf(dmarc_record):
|
||||
"""Returns the aspf value from a DMARC record"""
|
||||
if "aspf=" in str(dmarc_record):
|
||||
return str(dmarc_record).split("aspf=")[1].split(";")[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_dmarc_subdomain_policy(dmarc_record):
|
||||
"""Returns the policy to apply for subdomains from a DMARC record."""
|
||||
if "sp=" in str(dmarc_record):
|
||||
return str(dmarc_record).split("sp=")[1].split(";")[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_dmarc_forensic_reports(dmarc_record):
|
||||
"""Returns the email addresses to which forensic reports should be sent."""
|
||||
if "ruf=" in str(dmarc_record) and "fo=1" in str(dmarc_record):
|
||||
return str(dmarc_record).split("ruf=")[1].split(";")[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_dmarc_aggregate_reports(dmarc_record):
|
||||
"""Returns the email addresses to which aggregate reports should be sent."""
|
||||
if "rua=" in str(dmarc_record):
|
||||
return str(dmarc_record).split("rua=")[1].split(";")[0]
|
||||
else:
|
||||
return None
|
||||
58
libs/dns.py
58
libs/dns.py
@@ -1,58 +0,0 @@
|
||||
import dns.resolver
|
||||
import socket
|
||||
from . import spf, dmarc, bimi
|
||||
|
||||
|
||||
def get_soa_record(domain):
|
||||
"""Returns the SOA record of a given domain."""
|
||||
resolver = dns.resolver.Resolver()
|
||||
resolver.nameservers = ['1.1.1.1']
|
||||
try:
|
||||
query = resolver.resolve(domain, 'SOA')
|
||||
except:
|
||||
return None
|
||||
if query:
|
||||
for data in query:
|
||||
dns_server = str(data.mname)
|
||||
try:
|
||||
return socket.gethostbyname(dns_server)
|
||||
except:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def get_dns_server(domain):
|
||||
"""Finds the DNS server that serves the domain and returns it, along with any SPF or DMARC records."""
|
||||
SOA = get_soa_record(domain)
|
||||
spf_record = dmarc_record = partial_spf_record = partial_dmarc_record = bimi_record = None
|
||||
|
||||
if SOA:
|
||||
spf_record = spf.get_spf_record(domain, SOA)
|
||||
dmarc_record = dmarc.get_dmarc_record(domain, SOA)
|
||||
bimi_record = bimi.get_bimi_record(domain, SOA)
|
||||
if spf_record and dmarc_record:
|
||||
return SOA, spf_record, dmarc_record, bimi_record
|
||||
|
||||
for ip_address in ['1.1.1.1', '8.8.8.8', '9.9.9.9']:
|
||||
spf_record = spf.get_spf_record(domain, ip_address)
|
||||
dmarc_record = dmarc.get_dmarc_record(domain, ip_address)
|
||||
bimi_record = bimi.get_bimi_record(domain, SOA)
|
||||
if spf_record and dmarc_record:
|
||||
return ip_address, spf_record, dmarc_record, bimi_record
|
||||
if spf_record:
|
||||
partial_spf_record = spf_record
|
||||
if dmarc_record:
|
||||
partial_dmarc_record = dmarc_record
|
||||
|
||||
return '1.1.1.1', partial_spf_record, partial_dmarc_record, bimi_record
|
||||
|
||||
|
||||
def get_txt_record(domain, record_type):
|
||||
"""Returns the TXT record of a given type for a given domain."""
|
||||
resolver = dns.resolver.Resolver()
|
||||
resolver.nameservers = [get_dns_server(domain)]
|
||||
try:
|
||||
query = resolver.query(domain, record_type)
|
||||
return str(query[0])
|
||||
except:
|
||||
return None
|
||||
@@ -1,83 +0,0 @@
|
||||
def is_spoofable(domain, p, aspf, spf_record, spf_all, spf_includes, sp, pct):
|
||||
"""This function takes in DMARC and SPF data for a domain, as well as subdomain policy and percentage options,
|
||||
and determines if the domain is vulnerable to email spoofing. The function returns an integer value indicating
|
||||
the class of vulnerability.
|
||||
ID Handler:
|
||||
0: Indicates that spoofing is possible for the domain.
|
||||
1: Indicates that subdomain spoofing is possible for the domain.
|
||||
2: Indicates that organizational domain spoofing is possible for the domain.
|
||||
3: Indicates that spoofing might be possible for the domain.
|
||||
4: Indicates that spoofing might be possible (mailbox dependent) for the domain.
|
||||
5: Indicates that organizational domain spoofing may be possible for the domain.
|
||||
6: Indicates that subdomain spoofing might be possible (mailbox dependent) for the domain.
|
||||
7: Indicates that subdomain spoofing is possible, and organizational domain spoofing might be possible.
|
||||
8: Indicates that spoofing is not possible for the domain.
|
||||
"""
|
||||
try:
|
||||
if pct and int(pct) != 100:
|
||||
return 3
|
||||
elif spf_record is None:
|
||||
if p is None:
|
||||
return 0
|
||||
else:
|
||||
return 8
|
||||
elif spf_includes > 10 and p is None:
|
||||
return 0
|
||||
elif spf_all == "2many":
|
||||
if p == "none":
|
||||
return 3
|
||||
else:
|
||||
return 8
|
||||
elif spf_all and p is None:
|
||||
return 0
|
||||
elif spf_all == "-all":
|
||||
if p and aspf and sp == "none":
|
||||
return 1
|
||||
elif aspf is None and sp == "none":
|
||||
return 1
|
||||
elif p == "none" and (aspf == "r" or aspf is None) and sp is None:
|
||||
return 4
|
||||
elif p == "none" and aspf == "r" and (sp == "reject" or sp == "quarentine"):
|
||||
return 2
|
||||
elif p == "none" and aspf is None and (sp == "reject" or sp == "quarentine"):
|
||||
return 5
|
||||
elif p == "none" and aspf is None and sp == "none":
|
||||
return 7
|
||||
else:
|
||||
return 8
|
||||
elif spf_all == "~all":
|
||||
if p == "none" and sp == "reject" or sp == "quarentine":
|
||||
return 2
|
||||
elif p == "none" and sp is None:
|
||||
return 0
|
||||
elif p == "none" and sp == "none":
|
||||
return 7
|
||||
elif (p == "reject" or p == "quarentine") and aspf is None and sp == "none":
|
||||
return 1
|
||||
elif (p == "reject" or p == "quarentine") and aspf and sp == "none":
|
||||
return 1
|
||||
else:
|
||||
return 8
|
||||
elif spf_all == "?all":
|
||||
if (p == "reject" or p == "quarentine") and aspf and sp == "none":
|
||||
return 6
|
||||
elif (p == "reject" or p == "quarentine") and aspf is None and sp == "none":
|
||||
return 6
|
||||
elif p == "none" and aspf == "r" and sp is None:
|
||||
return 0
|
||||
elif p == "none" and aspf == "r" and sp == "none":
|
||||
return 7
|
||||
elif p == "none" and aspf == "s" or None and sp == "none":
|
||||
return 7
|
||||
elif p == "none" and aspf == "s" or None and sp is None:
|
||||
return 6
|
||||
elif p == "none" and aspf and (sp == "reject" or sp == "quarentine"):
|
||||
return 5
|
||||
elif p == "none" and aspf is None and sp == "reject":
|
||||
return 5
|
||||
else:
|
||||
return 8
|
||||
else:
|
||||
return 8
|
||||
except:
|
||||
print("If you hit this error message, Open an issue with your testcase.")
|
||||
119
libs/report.py
119
libs/report.py
@@ -1,119 +0,0 @@
|
||||
from colorama import Fore, Style
|
||||
from colorama import init as color_init
|
||||
import os
|
||||
import pandas as pd
|
||||
|
||||
color_init()
|
||||
|
||||
|
||||
def output_good(line):
|
||||
print(Fore.GREEN + Style.BRIGHT + "[+]" + Style.RESET_ALL, line)
|
||||
|
||||
|
||||
def output_warning(line):
|
||||
print(Fore.YELLOW + Style.BRIGHT + "[?]" + Style.RESET_ALL, line)
|
||||
|
||||
|
||||
def output_bad(line):
|
||||
print(Fore.RED + Style.BRIGHT + "[-]" + Style.RESET_ALL, line)
|
||||
|
||||
|
||||
def output_indifferent(line):
|
||||
print(Fore.BLUE + Style.BRIGHT + "[*]" + Style.RESET_ALL, line)
|
||||
|
||||
|
||||
def output_error(line):
|
||||
print(Fore.RED + Style.BRIGHT + "[-] !!! " +
|
||||
Style.NORMAL, line, Style.BRIGHT + "!!!\n")
|
||||
|
||||
|
||||
def output_info(line):
|
||||
print(Fore.WHITE + Style.BRIGHT + "[*]" + Style.RESET_ALL, line)
|
||||
|
||||
|
||||
def write_to_excel(data):
|
||||
"""This function writes a DataFrame of data to an Excel file.
|
||||
If the file does not already exist, it creates the file, and if the file already exists, it appends the new data to the existing data."""
|
||||
file_name = "report.xlsx"
|
||||
if not os.path.exists(file_name):
|
||||
open(file_name, 'w').close()
|
||||
|
||||
if os.path.getsize(file_name) > 0:
|
||||
existing_df = pd.read_excel(file_name)
|
||||
new_df = pd.DataFrame(data)
|
||||
combined_df = pd.concat([existing_df, new_df])
|
||||
combined_df.to_excel(file_name, index=False)
|
||||
else:
|
||||
df = pd.DataFrame(data)
|
||||
df.to_excel(file_name, index=False)
|
||||
|
||||
|
||||
def printer(domain, subdomain, dns_server, spf_record, spf_all, spf_includes, dmarc_record, p, pct, aspf, sp, fo, rua, bimi_record, vbimi, location, authority, spoofable):
|
||||
"""This function is a utility function that takes in various parameters related to the
|
||||
results of DMARC and SPF checks and outputs the results to the console in a human-readable format.
|
||||
|
||||
Printer ID Handler:
|
||||
0: Indicates that spoofing is possible for the domain.
|
||||
1: Indicates that subdomain spoofing is possible for the domain.
|
||||
2: Indicates that organizational domain spoofing is possible for the domain.
|
||||
3: Indicates that spoofing might be possible for the domain.
|
||||
4: Indicates that spoofing might be possible (mailbox dependent) for the domain.
|
||||
5: Indicates that organizational domain spoofing may be possible for the domain.
|
||||
6: Indicates that subdomain spoofing might be possible (mailbox dependent) for the domain.
|
||||
7: Indicates that subdomain spoofing is possible, and organizational domain spoofing might be possible.
|
||||
8: Indicates that spoofing is not possible for the domain.
|
||||
"""
|
||||
output_indifferent(f"Domain: {domain}")
|
||||
output_indifferent(f"Is subdomain: {subdomain}")
|
||||
output_indifferent(f"DNS Server: {dns_server}")
|
||||
|
||||
if spf_record:
|
||||
output_info(f"SPF record: {spf_record}")
|
||||
if spf_all is None:
|
||||
output_info("SPF does not contain an `All` items.")
|
||||
elif spf_all == "2many":
|
||||
output_warning("SPF record contains multiple `All` items.")
|
||||
else:
|
||||
output_info(f"SPF all record: {spf_all}")
|
||||
output_info(f"SPF include count: {spf_includes}" if spf_includes <=
|
||||
10 else f"Too many SPF include lookups {spf_includes}.")
|
||||
else:
|
||||
output_warning("No SPF record found.")
|
||||
|
||||
if dmarc_record:
|
||||
output_info(f"DMARC record: {dmarc_record}")
|
||||
output_info(
|
||||
f"Found DMARC policy: {p}" if p else "No DMARC policy found.")
|
||||
output_info(
|
||||
f"Found DMARC pct: {pct}" if pct else "No DMARC pct found.")
|
||||
output_info(
|
||||
f"Found DMARC aspf: {aspf}" if aspf else "No DMARC aspf found.")
|
||||
output_info(
|
||||
f"Found DMARC subdomain policy: {sp}" if sp else "No DMARC subdomain policy found.")
|
||||
output_indifferent(
|
||||
f"Forensics reports will be sent: {fo}" if fo else "No DMARC forensics report location found.")
|
||||
output_indifferent(
|
||||
f"Aggregate reports will be sent to: {rua}" if rua else "No DMARC aggregate report location found.")
|
||||
else:
|
||||
output_warning("No DMARC record found.")
|
||||
|
||||
if(bimi_record):
|
||||
output_info(f"BIMI record : {bimi_record}")
|
||||
output_info(f"BIMI version : {vbimi}")
|
||||
output_info(f"BIMI location : {location}")
|
||||
output_info(f"BIMI authority : {authority}")
|
||||
|
||||
if spoofable in [0, 1, 2, 3, 4, 5, 6, 7, 8]:
|
||||
if spoofable == 8:
|
||||
output_bad("Spoofing not possible for " + domain)
|
||||
else:
|
||||
output_good("Spoofing possible for " + domain
|
||||
if spoofable == 0 else "Subdomain spoofing possible for " + domain
|
||||
if spoofable == 1 else "Organizational domain spoofing possible for " + domain
|
||||
if spoofable == 2 else "Spoofing might be possible for " + domain
|
||||
if spoofable == 3 else "Spoofing might be possible (Mailbox dependant) for " + domain
|
||||
if spoofable == 4 else "Organizational domain spoofing may be possible for " + domain
|
||||
if spoofable == 5 else "Subdomain spoofing might be possible (Mailbox dependant) for " + domain
|
||||
if spoofable == 6 else "Subdomain spoofing might be possible (Mailbox dependant) for " + domain
|
||||
if spoofable == 7 else "")
|
||||
print() # padding
|
||||
49
libs/spf.py
49
libs/spf.py
@@ -1,49 +0,0 @@
|
||||
import re
|
||||
import dns.resolver
|
||||
|
||||
|
||||
def get_spf_record(domain, dns_server):
|
||||
"""Returns the SPF record for a given domain."""
|
||||
try:
|
||||
resolver = dns.resolver.Resolver()
|
||||
resolver.nameservers = [dns_server, '1.1.1.1', '8.8.8.8']
|
||||
query_result = resolver.resolve(domain, 'TXT')
|
||||
for record in query_result:
|
||||
if 'spf1' in str(record):
|
||||
spf_record = str(record).replace('"', '')
|
||||
return spf_record
|
||||
return None
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def get_spf_all_string(spf_record):
|
||||
"""Returns the string value of the all mechanism in the SPF record."""
|
||||
all_matches = re.findall(r'[-~?] ?a ?l ?l', spf_record)
|
||||
if len(all_matches) == 1:
|
||||
return all_matches[0]
|
||||
elif len(all_matches) > 1:
|
||||
return '2many'
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_spf_includes(domain, count=0):
|
||||
"""Returns the number of includes in the SPF record for a given domain."""
|
||||
if count > 10:
|
||||
return count
|
||||
try:
|
||||
spf_record = get_spf_record(domain, '1.1.1.1')
|
||||
if spf_record:
|
||||
count += len(re.compile("[ ,+]a[ ,:]").findall(spf_record))
|
||||
count += len(re.compile("[ ,+]mx[ ,:]").findall(spf_record))
|
||||
count += len(re.compile("[ ]ptr[ ]").findall(spf_record))
|
||||
count += len(re.compile("exists[:]").findall(spf_record))
|
||||
for item in spf_record.split(' '):
|
||||
if "include:" in item:
|
||||
url = item.replace('include:', '')
|
||||
count = get_spf_includes(url, count + 1)
|
||||
except:
|
||||
pass
|
||||
# print("Could not find SPF record for " + domain)
|
||||
return count
|
||||
1
modules/__init__.py
Normal file
1
modules/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# modules/__init__.py
|
||||
62
modules/bimi.py
Normal file
62
modules/bimi.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# modules/bimi.py
|
||||
|
||||
import dns.resolver
|
||||
|
||||
|
||||
class BIMI:
|
||||
def __init__(self, domain, dns_server=None):
|
||||
self.domain = domain
|
||||
self.dns_server = dns_server
|
||||
self.bimi_record = self.get_bimi_record()
|
||||
self.version = None
|
||||
self.location = None
|
||||
self.authority = None
|
||||
|
||||
if self.bimi_record:
|
||||
self.version = self.get_bimi_version()
|
||||
self.location = self.get_bimi_location()
|
||||
self.authority = self.get_bimi_authority()
|
||||
|
||||
def get_bimi_record(self):
|
||||
"""Returns the BIMI record for the domain."""
|
||||
try:
|
||||
resolver = dns.resolver.Resolver()
|
||||
if self.dns_server:
|
||||
resolver.nameservers = [self.dns_server]
|
||||
bimi = resolver.resolve(f"default._bimi.{self.domain}", "TXT")
|
||||
for record in bimi:
|
||||
if "v=BIMI" in str(record):
|
||||
return record
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_bimi_version(self):
|
||||
"""Returns the version value from a BIMI record."""
|
||||
if "v=" in str(self.bimi_record):
|
||||
return str(self.bimi_record).split("v=")[1].split(";")[0]
|
||||
return None
|
||||
|
||||
def get_bimi_location(self):
|
||||
"""Returns the location value from a BIMI record."""
|
||||
if "l=" in str(self.bimi_record):
|
||||
return str(self.bimi_record).split("l=")[1].split(";")[0]
|
||||
return None
|
||||
|
||||
def get_bimi_authority(self):
|
||||
"""Returns the authority value from a BIMI record."""
|
||||
if "a=" in str(self.bimi_record):
|
||||
return str(self.bimi_record).split("a=")[1].split(";")[0]
|
||||
return None
|
||||
|
||||
def get_bimi_details(self):
|
||||
"""Returns a tuple containing version, location, and authority from a BIMI record."""
|
||||
return self.version, self.location, self.authority
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"BIMI Record: {self.bimi_record}\n"
|
||||
f"Version: {self.version}\n"
|
||||
f"Location: {self.location}\n"
|
||||
f"Authority: {self.authority}"
|
||||
)
|
||||
94
modules/dmarc.py
Normal file
94
modules/dmarc.py
Normal file
@@ -0,0 +1,94 @@
|
||||
# modules/dmarc.py
|
||||
|
||||
import dns.resolver
|
||||
import tldextract
|
||||
|
||||
|
||||
class DMARC:
|
||||
def __init__(self, domain, dns_server=None):
|
||||
self.domain = domain
|
||||
self.dns_server = dns_server
|
||||
self.dmarc_record = self.get_dmarc_record()
|
||||
self.policy = None
|
||||
self.pct = None
|
||||
self.aspf = None
|
||||
self.sp = None
|
||||
self.fo = None
|
||||
self.rua = None
|
||||
|
||||
if self.dmarc_record:
|
||||
self.policy = self.get_dmarc_policy()
|
||||
self.pct = self.get_dmarc_pct()
|
||||
self.aspf = self.get_dmarc_aspf()
|
||||
self.sp = self.get_dmarc_subdomain_policy()
|
||||
self.fo = self.get_dmarc_forensic_reports()
|
||||
self.rua = self.get_dmarc_aggregate_reports()
|
||||
|
||||
def get_dmarc_record(self):
|
||||
"""Returns the DMARC record for the domain."""
|
||||
subdomain = tldextract.extract(self.domain).registered_domain
|
||||
if subdomain != self.domain:
|
||||
return self.get_dmarc_record_for_domain(subdomain)
|
||||
|
||||
return self.get_dmarc_record_for_domain(self.domain)
|
||||
|
||||
def get_dmarc_record_for_domain(self, domain):
|
||||
try:
|
||||
resolver = dns.resolver.Resolver()
|
||||
if self.dns_server:
|
||||
resolver.nameservers = [self.dns_server]
|
||||
dmarc = resolver.resolve(f"_dmarc.{domain}", "TXT")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
for dns_data in dmarc:
|
||||
if "DMARC1" in str(dns_data):
|
||||
return str(dns_data).replace('"', "")
|
||||
return None
|
||||
|
||||
def get_dmarc_policy(self):
|
||||
"""Returns the policy value from a DMARC record."""
|
||||
if "p=" in str(self.dmarc_record):
|
||||
return str(self.dmarc_record).split("p=")[1].split(";")[0]
|
||||
return None
|
||||
|
||||
def get_dmarc_pct(self):
|
||||
"""Returns the pct value from a DMARC record."""
|
||||
if "pct=" in str(self.dmarc_record):
|
||||
return str(self.dmarc_record).split("pct=")[1].split(";")[0]
|
||||
return None
|
||||
|
||||
def get_dmarc_aspf(self):
|
||||
"""Returns the aspf value from a DMARC record"""
|
||||
if "aspf=" in str(self.dmarc_record):
|
||||
return str(self.dmarc_record).split("aspf=")[1].split(";")[0]
|
||||
return None
|
||||
|
||||
def get_dmarc_subdomain_policy(self):
|
||||
"""Returns the policy to apply for subdomains from a DMARC record."""
|
||||
if "sp=" in str(self.dmarc_record):
|
||||
return str(self.dmarc_record).split("sp=")[1].split(";")[0]
|
||||
return None
|
||||
|
||||
def get_dmarc_forensic_reports(self):
|
||||
"""Returns the email addresses to which forensic reports should be sent."""
|
||||
if "ruf=" in str(self.dmarc_record) and "fo=1" in str(self.dmarc_record):
|
||||
return str(self.dmarc_record).split("ruf=")[1].split(";")[0]
|
||||
return None
|
||||
|
||||
def get_dmarc_aggregate_reports(self):
|
||||
"""Returns the email addresses to which aggregate reports should be sent."""
|
||||
if "rua=" in str(self.dmarc_record):
|
||||
return str(self.dmarc_record).split("rua=")[1].split(";")[0]
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"DMARC Record: {self.dmarc_record}\n"
|
||||
f"Policy: {self.policy}\n"
|
||||
f"Pct: {self.pct}\n"
|
||||
f"ASPF: {self.aspf}\n"
|
||||
f"Subdomain Policy: {self.sp}\n"
|
||||
f"Forensic Report URI: {self.fo}\n"
|
||||
f"Aggregate Report URI: {self.rua}"
|
||||
)
|
||||
76
modules/dns.py
Normal file
76
modules/dns.py
Normal file
@@ -0,0 +1,76 @@
|
||||
# modules/dns.py
|
||||
|
||||
import dns.resolver
|
||||
import socket
|
||||
from .spf import SPF
|
||||
from .dmarc import DMARC
|
||||
from .bimi import BIMI
|
||||
|
||||
|
||||
class DNS:
|
||||
def __init__(self, domain):
|
||||
self.domain = domain
|
||||
self.soa_record = None
|
||||
self.dns_server = None
|
||||
self.spf_record = None
|
||||
self.dmarc_record = None
|
||||
self.bimi_record = None
|
||||
|
||||
self.get_soa_record()
|
||||
self.get_dns_server()
|
||||
|
||||
def get_soa_record(self):
|
||||
"""Sets the SOA record and DNS server of a given domain."""
|
||||
resolver = dns.resolver.Resolver()
|
||||
resolver.nameservers = ["1.1.1.1"]
|
||||
try:
|
||||
query = resolver.resolve(self.domain, "SOA")
|
||||
except Exception:
|
||||
return
|
||||
if query:
|
||||
for data in query:
|
||||
dns_server = str(data.mname)
|
||||
try:
|
||||
self.soa_record = socket.gethostbyname(dns_server)
|
||||
self.dns_server = self.soa_record
|
||||
except Exception:
|
||||
self.soa_record = None
|
||||
|
||||
def get_dns_server(self):
|
||||
"""Finds the DNS server that serves the domain and retrieves associated SPF, DMARC, and BIMI records."""
|
||||
if self.soa_record:
|
||||
self.spf_record = SPF(self.domain, self.soa_record)
|
||||
self.dmarc_record = DMARC(self.domain, self.soa_record)
|
||||
self.bimi_record = BIMI(self.domain, self.soa_record)
|
||||
if self.spf_record.spf_record and self.dmarc_record.dmarc_record:
|
||||
return
|
||||
|
||||
for ip_address in ["1.1.1.1", "8.8.8.8", "9.9.9.9"]:
|
||||
self.spf_record = SPF(self.domain, ip_address)
|
||||
self.dmarc_record = DMARC(self.domain, ip_address)
|
||||
self.bimi_record = BIMI(self.domain, ip_address)
|
||||
if self.spf_record.spf_record and self.dmarc_record.dmarc_record:
|
||||
self.dns_server = ip_address
|
||||
return
|
||||
|
||||
self.dns_server = "1.1.1.1"
|
||||
|
||||
def get_txt_record(self, record_type):
|
||||
"""Returns the TXT record of a given type for the domain."""
|
||||
resolver = dns.resolver.Resolver()
|
||||
resolver.nameservers = [self.dns_server]
|
||||
try:
|
||||
query = resolver.query(self.domain, record_type)
|
||||
return str(query[0])
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"Domain: {self.domain}\n"
|
||||
f"SOA Record: {self.soa_record}\n"
|
||||
f"DNS Server: {self.dns_server}\n"
|
||||
f"SPF Record: {self.spf_record.spf_record}\n"
|
||||
f"DMARC Record: {self.dmarc_record.dmarc_record}\n"
|
||||
f"BIMI Record: {self.bimi_record.bimi_record}"
|
||||
)
|
||||
130
modules/report.py
Normal file
130
modules/report.py
Normal file
@@ -0,0 +1,130 @@
|
||||
# modules/report.py
|
||||
|
||||
import os
|
||||
import pandas as pd
|
||||
from colorama import init, Fore, Style
|
||||
|
||||
# Initialize colorama
|
||||
init()
|
||||
|
||||
|
||||
def output_message(symbol, message, level="info"):
|
||||
"""Generic function to print messages with different colors and symbols based on the level."""
|
||||
colors = {
|
||||
"good": Fore.GREEN + Style.BRIGHT,
|
||||
"warning": Fore.YELLOW + Style.BRIGHT,
|
||||
"bad": Fore.RED + Style.BRIGHT,
|
||||
"indifferent": Fore.BLUE + Style.BRIGHT,
|
||||
"error": Fore.RED + Style.BRIGHT + "!!! ",
|
||||
"info": Fore.WHITE + Style.BRIGHT,
|
||||
}
|
||||
color = colors.get(level, Fore.WHITE + Style.BRIGHT)
|
||||
print(color + f"{symbol} {message}" + Style.RESET_ALL)
|
||||
|
||||
|
||||
def write_to_excel(data, file_name="output.xlsx"):
|
||||
"""Writes a DataFrame of data to an Excel file, appending if the file exists."""
|
||||
if os.path.exists(file_name) and os.path.getsize(file_name) > 0:
|
||||
existing_df = pd.read_excel(file_name)
|
||||
new_df = pd.DataFrame(data)
|
||||
combined_df = pd.concat([existing_df, new_df])
|
||||
combined_df.to_excel(file_name, index=False)
|
||||
else:
|
||||
pd.DataFrame(data).to_excel(file_name, index=False)
|
||||
|
||||
|
||||
def printer(**kwargs):
|
||||
"""Utility function to print the results of DMARC, SPF, and BIMI checks in the original format."""
|
||||
domain = kwargs.get("DOMAIN")
|
||||
subdomain = kwargs.get("DOMAIN_TYPE") == "subdomain"
|
||||
dns_server = kwargs.get("DNS_SERVER")
|
||||
spf_record = kwargs.get("SPF")
|
||||
spf_all = kwargs.get("SPF_MULTIPLE_ALLS")
|
||||
spf_dns_query_count = kwargs.get("SPF_NUM_DNS_QUERIES")
|
||||
dmarc_record = kwargs.get("DMARC")
|
||||
p = kwargs.get("DMARC_POLICY")
|
||||
pct = kwargs.get("DMARC_PCT")
|
||||
aspf = kwargs.get("DMARC_ASPF")
|
||||
sp = kwargs.get("DMARC_SP")
|
||||
fo = kwargs.get("DMARC_FORENSIC_REPORT")
|
||||
rua = kwargs.get("DMARC_AGGREGATE_REPORT")
|
||||
bimi_record = kwargs.get("BIMI_RECORD")
|
||||
vbimi = kwargs.get("BIMI_VERSION")
|
||||
location = kwargs.get("BIMI_LOCATION")
|
||||
authority = kwargs.get("BIMI_AUTHORITY")
|
||||
spoofable = kwargs.get("SPOOFING_POSSIBLE")
|
||||
spoofing_type = kwargs.get("SPOOFING_TYPE")
|
||||
|
||||
output_message("[*]", f"Domain: {domain}", "indifferent")
|
||||
output_message("[*]", f"Is subdomain: {subdomain}", "indifferent")
|
||||
output_message("[*]", f"DNS Server: {dns_server}", "indifferent")
|
||||
|
||||
if spf_record:
|
||||
output_message("[*]", f"SPF record: {spf_record}", "info")
|
||||
if spf_all is None:
|
||||
output_message("[*]", "SPF does not contain an `All` item.", "info")
|
||||
elif spf_all == "2many":
|
||||
output_message(
|
||||
"[?]", "SPF record contains multiple `All` items.", "warning"
|
||||
)
|
||||
else:
|
||||
output_message("[*]", f"SPF all record: {spf_all}", "info")
|
||||
output_message(
|
||||
"[*]",
|
||||
f"SPF DNS query count: {spf_dns_query_count}"
|
||||
if spf_dns_query_count <= 10
|
||||
else f"Too many SPF DNS query lookups {spf_dns_query_count}.",
|
||||
"info",
|
||||
)
|
||||
else:
|
||||
output_message("[?]", "No SPF record found.", "warning")
|
||||
|
||||
if dmarc_record:
|
||||
output_message("[*]", f"DMARC record: {dmarc_record}", "info")
|
||||
output_message(
|
||||
"[*]", f"Found DMARC policy: {p}" if p else "No DMARC policy found.", "info"
|
||||
)
|
||||
output_message(
|
||||
"[*]", f"Found DMARC pct: {pct}" if pct else "No DMARC pct found.", "info"
|
||||
)
|
||||
output_message(
|
||||
"[*]",
|
||||
f"Found DMARC aspf: {aspf}" if aspf else "No DMARC aspf found.",
|
||||
"info",
|
||||
)
|
||||
output_message(
|
||||
"[*]",
|
||||
f"Found DMARC subdomain policy: {sp}"
|
||||
if sp
|
||||
else "No DMARC subdomain policy found.",
|
||||
"info",
|
||||
)
|
||||
output_message(
|
||||
"[*]",
|
||||
f"Forensics reports will be sent: {fo}"
|
||||
if fo
|
||||
else "No DMARC forensics report location found.",
|
||||
"indifferent",
|
||||
)
|
||||
output_message(
|
||||
"[*]",
|
||||
f"Aggregate reports will be sent to: {rua}"
|
||||
if rua
|
||||
else "No DMARC aggregate report location found.",
|
||||
"indifferent",
|
||||
)
|
||||
else:
|
||||
output_message("[?]", "No DMARC record found.", "warning")
|
||||
|
||||
if bimi_record:
|
||||
output_message("[*]", f"BIMI record: {bimi_record}", "info")
|
||||
output_message("[*]", f"BIMI version: {vbimi}", "info")
|
||||
output_message("[*]", f"BIMI location: {location}", "info")
|
||||
output_message("[*]", f"BIMI authority: {authority}", "info")
|
||||
|
||||
if spoofing_type:
|
||||
level = "good" if spoofable else "bad"
|
||||
symbol = "[+]" if level == "good" else "[-]"
|
||||
output_message(symbol, spoofing_type, level)
|
||||
|
||||
print() # Padding
|
||||
102
modules/spf.py
Normal file
102
modules/spf.py
Normal file
@@ -0,0 +1,102 @@
|
||||
# modules/spf.py
|
||||
|
||||
import dns.resolver
|
||||
import re
|
||||
|
||||
|
||||
class SPF:
|
||||
def __init__(self, domain, dns_server=None):
|
||||
self.domain = domain
|
||||
self.dns_server = dns_server
|
||||
self.spf_record = self.get_spf_record()
|
||||
self.all_mechanism = None
|
||||
self.spf_dns_query_count = 0
|
||||
self.too_many_dns_queries = False
|
||||
|
||||
if self.spf_record:
|
||||
self.all_mechanism = self.get_spf_all_string()
|
||||
self.spf_dns_query_count = self.get_spf_dns_queries()
|
||||
self.too_many_dns_queries = self.spf_dns_query_count > 10
|
||||
|
||||
def get_spf_record(self, domain=None):
|
||||
"""Fetches the SPF record for the specified domain."""
|
||||
try:
|
||||
if not domain:
|
||||
domain = self.domain
|
||||
resolver = dns.resolver.Resolver()
|
||||
resolver.nameservers = [self.dns_server, "1.1.1.1", "8.8.8.8"]
|
||||
query_result = resolver.resolve(domain, "TXT")
|
||||
for record in query_result:
|
||||
if "spf1" in str(record):
|
||||
spf_record = str(record).replace('"', "")
|
||||
return spf_record
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_spf_all_string(self):
|
||||
"""Returns the string value of the 'all' mechanism in the SPF record."""
|
||||
|
||||
spf_record = self.spf_record
|
||||
visited_domains = set()
|
||||
|
||||
while spf_record:
|
||||
all_matches = re.findall(r"[-~?+]all", spf_record)
|
||||
if len(all_matches) == 1:
|
||||
return all_matches[0]
|
||||
elif len(all_matches) > 1:
|
||||
return "2many"
|
||||
|
||||
redirect_match = re.search(r"redirect=([\w.-]+)", spf_record)
|
||||
if redirect_match:
|
||||
redirect_domain = redirect_match.group(1)
|
||||
if redirect_domain in visited_domains:
|
||||
break # Prevent infinite loops in case of circular redirects
|
||||
visited_domains.add(redirect_domain)
|
||||
spf_record = self.get_spf_record(redirect_domain)
|
||||
else:
|
||||
break
|
||||
|
||||
return None
|
||||
|
||||
def get_spf_dns_queries(self):
|
||||
"""Returns the number of dns queries, redirects, and other mechanisms in the SPF record for a given domain."""
|
||||
|
||||
def count_dns_queries(spf_record):
|
||||
count = 0
|
||||
for item in spf_record.split():
|
||||
if item.startswith("include:") or item.startswith("redirect="):
|
||||
if item.startswith("include:"):
|
||||
url = item.replace("include:", "")
|
||||
elif item.startswith("redirect="):
|
||||
url = item.replace("redirect=", "")
|
||||
|
||||
count += 1
|
||||
try:
|
||||
# Recursively fetch and count dns queries or redirects in the SPF record of the referenced domain
|
||||
answers = dns.resolver.resolve(url, "TXT")
|
||||
for rdata in answers:
|
||||
for txt_string in rdata.strings:
|
||||
txt_record = txt_string.decode("utf-8")
|
||||
if txt_record.startswith("v=spf1"):
|
||||
count += count_dns_queries(txt_record)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Count occurrences of 'a', 'mx', 'ptr', and 'exists' mechanisms
|
||||
count += len(re.findall(r"[ ,+]a[ ,:]", spf_record))
|
||||
count += len(re.findall(r"[ ,+]mx[ ,:]", spf_record))
|
||||
count += len(re.findall(r"[ ]ptr[ ]", spf_record))
|
||||
count += len(re.findall(r"exists[:]", spf_record))
|
||||
|
||||
return count
|
||||
|
||||
return count_dns_queries(self.spf_record)
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"SPF Record: {self.spf_record}\n"
|
||||
f"All Mechanism: {self.all_mechanism}\n"
|
||||
f"DNS Query Count: {self.spf_dns_query_count}\n"
|
||||
f"Too Many DNS Queries: {self.too_many_dns_queries}"
|
||||
)
|
||||
187
modules/spoofing.py
Normal file
187
modules/spoofing.py
Normal file
@@ -0,0 +1,187 @@
|
||||
# modules/spoofing.py
|
||||
|
||||
import tldextract
|
||||
from .syntax import validate_record_syntax
|
||||
|
||||
|
||||
class Spoofing:
|
||||
def __init__(
|
||||
self,
|
||||
domain,
|
||||
dmarc_record,
|
||||
p,
|
||||
aspf,
|
||||
spf_record,
|
||||
spf_all,
|
||||
spf_dns_queries,
|
||||
sp,
|
||||
pct,
|
||||
):
|
||||
self.domain = domain
|
||||
self.dmarc_record = dmarc_record
|
||||
self.p = p
|
||||
self.aspf = aspf
|
||||
self.spf_record = spf_record
|
||||
self.spf_all = spf_all
|
||||
self.spf_dns_queries = spf_dns_queries
|
||||
self.sp = sp
|
||||
self.pct = pct
|
||||
self.domain_type = self.get_domain_type()
|
||||
self.spoofable = self.is_spoofable()
|
||||
self.spoofing_possible, self.spoofing_type = self.evaluate_spoofing()
|
||||
|
||||
def get_domain_type(self):
|
||||
"""Determines whether the domain is a domain or subdomain."""
|
||||
subdomain = bool(tldextract.extract(self.domain).subdomain)
|
||||
return "subdomain" if subdomain else "domain"
|
||||
|
||||
def is_spoofable(self):
|
||||
"""Determines the spoofability based on DMARC and SPF data."""
|
||||
try:
|
||||
if self.pct and int(self.pct) != 100:
|
||||
return 3
|
||||
elif self.spf_record is None:
|
||||
if self.p is None:
|
||||
return 0
|
||||
elif self.p == "none":
|
||||
return 4
|
||||
else:
|
||||
return 8
|
||||
elif self.spf_dns_queries > 10 and self.p is None:
|
||||
return 0
|
||||
elif self.spf_all == "2many":
|
||||
if self.p == "none":
|
||||
return 3
|
||||
else:
|
||||
return 8
|
||||
elif self.spf_all and self.p is None:
|
||||
return 0
|
||||
elif self.spf_all == "-all":
|
||||
if self.p and self.aspf and self.sp == "none":
|
||||
return 1
|
||||
elif self.aspf is None and self.sp == "none":
|
||||
return 1
|
||||
elif (
|
||||
self.p == "none"
|
||||
and (self.aspf == "r" or self.aspf is None)
|
||||
and self.sp is None
|
||||
):
|
||||
return 4
|
||||
elif (
|
||||
self.p == "none"
|
||||
and self.aspf == "r"
|
||||
and (self.sp == "reject" or self.sp == "quarantine")
|
||||
):
|
||||
return 2
|
||||
elif (
|
||||
self.p == "none"
|
||||
and self.aspf is None
|
||||
and (self.sp == "reject" or self.sp == "quarantine")
|
||||
):
|
||||
return 5
|
||||
elif self.p == "none" and self.aspf is None and self.sp == "none":
|
||||
return 7
|
||||
else:
|
||||
return 8
|
||||
elif self.spf_all == "~all":
|
||||
if self.p == "none" and self.sp == "reject" or self.sp == "quarantine":
|
||||
return 2
|
||||
elif self.p == "none" and self.sp is None:
|
||||
return 0
|
||||
elif self.p == "none" and self.sp == "none":
|
||||
return 7
|
||||
elif (
|
||||
(self.p == "reject" or self.p == "quarantine")
|
||||
and self.aspf is None
|
||||
and self.sp == "none"
|
||||
):
|
||||
return 1
|
||||
elif (
|
||||
(self.p == "reject" or self.p == "quarantine")
|
||||
and self.aspf
|
||||
and self.sp == "none"
|
||||
):
|
||||
return 1
|
||||
else:
|
||||
return 8
|
||||
elif self.spf_all == "?all":
|
||||
if (
|
||||
(self.p == "reject" or self.p == "quarantine")
|
||||
and self.aspf
|
||||
and self.sp == "none"
|
||||
):
|
||||
return 6
|
||||
elif (
|
||||
(self.p == "reject" or self.p == "quarantine")
|
||||
and self.aspf is None
|
||||
and self.sp == "none"
|
||||
):
|
||||
return 6
|
||||
elif self.p == "none" and self.aspf == "r" and self.sp is None:
|
||||
return 0
|
||||
elif self.p == "none" and self.aspf == "r" and self.sp == "none":
|
||||
return 7
|
||||
elif (
|
||||
self.p == "none" and self.aspf == "s" or None and self.sp == "none"
|
||||
):
|
||||
return 7
|
||||
elif self.p == "none" and self.aspf == "s" or None and self.sp is None:
|
||||
return 6
|
||||
elif (
|
||||
self.p == "none"
|
||||
and self.aspf
|
||||
and (self.sp == "reject" or self.sp == "quarantine")
|
||||
):
|
||||
return 5
|
||||
elif self.p == "none" and self.aspf is None and self.sp == "reject":
|
||||
return 5
|
||||
else:
|
||||
return 8
|
||||
else:
|
||||
return 8
|
||||
except Exception:
|
||||
# If you are here, this means you caught a domain with a syntax error!
|
||||
spf_valid = validate_record_syntax(self.spf_record, "SPF")
|
||||
dmarc_valid = validate_record_syntax(self.dmarc_record, "DMARC")
|
||||
|
||||
if (not spf_valid and not dmarc_valid) or (spf_valid and not dmarc_valid):
|
||||
return 0
|
||||
elif not spf_valid and dmarc_valid and self.p == "none":
|
||||
return 3
|
||||
else:
|
||||
return 8
|
||||
|
||||
def evaluate_spoofing(self):
|
||||
"""Evaluates and returns whether spoofing is possible and the type of spoofing."""
|
||||
spoofing_types = {
|
||||
0: f"Spoofing possible for {self.domain}.",
|
||||
1: f"Subdomain spoofing possible for {self.domain}.",
|
||||
2: f"Organizational domain spoofing possible for {self.domain}.",
|
||||
3: f"Spoofing might be possible for {self.domain}.",
|
||||
4: f"Spoofing might be possible (Mailbox dependent) for {self.domain}.",
|
||||
5: f"Organizational domain spoofing might be possible for {self.domain}.",
|
||||
6: f"Subdomain spoofing might be possible (Mailbox dependent) for {self.domain}.",
|
||||
7: f"Subdomain spoofing is possible and organizational domain spoofing might be possible for {self.domain}.",
|
||||
8: f"Spoofing is not possible for {self.domain}.",
|
||||
}
|
||||
|
||||
spoofing_type = spoofing_types.get(
|
||||
self.spoofable, f"Unknown spoofing type for {self.domain}."
|
||||
)
|
||||
|
||||
if self.spoofable in {0, 1, 3, 7}:
|
||||
spoofing_possible = True
|
||||
elif self.spoofable == 8:
|
||||
spoofing_possible = False
|
||||
else:
|
||||
spoofing_possible = None # "maybe"
|
||||
|
||||
return spoofing_possible, spoofing_type
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"Domain: {self.domain}\n"
|
||||
f"Domain Type: {self.domain_type}\n"
|
||||
f"Spoofing Possible: {self.spoofing_possible}\n"
|
||||
f"Spoofing Type: {self.spoofing_type}"
|
||||
)
|
||||
96
modules/syntax.py
Normal file
96
modules/syntax.py
Normal file
@@ -0,0 +1,96 @@
|
||||
# modules/syntax.py
|
||||
import re
|
||||
|
||||
|
||||
def validate_record_syntax(record, record_type):
|
||||
"""Validate the syntax of a DNS record (SPF or DMARC)."""
|
||||
|
||||
if record_type == "SPF":
|
||||
mechanism_patterns = {
|
||||
"all": r"^all$",
|
||||
"include": r"^include:[\w\.\-]+\.[a-zA-Z]{2,}$",
|
||||
"a": r"^a(:[\w\.\-]+)?$",
|
||||
"mx": r"^mx(:[\w\.\-]+)?$",
|
||||
"ptr": r"^ptr(:[\w\.\-]+)?$",
|
||||
"ip4": r"^ip4:(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$",
|
||||
"ip6": r"^ip6:[a-fA-F0-9:]+(\/\d{1,3})?$",
|
||||
"exists": r"^exists:[\w\.\-]+\.[a-zA-Z]{2,}$",
|
||||
}
|
||||
|
||||
modifier_patterns = {
|
||||
"redirect": r"^redirect=[\w\.\-]+\.[a-zA-Z]{2,}$",
|
||||
"exp": r"^exp=[\w\.\-]+\.[a-zA-Z]{2,}$",
|
||||
}
|
||||
|
||||
elements = record.split()
|
||||
|
||||
if len(elements) == 0 or elements[0].strip().lower() != "v=spf1":
|
||||
return False
|
||||
|
||||
for element in elements[1:]:
|
||||
element = element.strip()
|
||||
|
||||
if ":" in element:
|
||||
mechanism, value = element.split(":", 1)
|
||||
elif "=" in element:
|
||||
modifier, value = element.split("=", 1)
|
||||
mechanism = modifier
|
||||
else:
|
||||
mechanism = element
|
||||
value = None
|
||||
|
||||
if mechanism in mechanism_patterns:
|
||||
pattern = mechanism_patterns[mechanism]
|
||||
if value:
|
||||
if not re.match(pattern, element):
|
||||
return False
|
||||
elif not re.match(r"^" + mechanism + r"$", element):
|
||||
return False
|
||||
|
||||
elif mechanism in modifier_patterns:
|
||||
pattern = modifier_patterns[mechanism]
|
||||
if not re.match(pattern, element):
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
elif record_type == "DMARC":
|
||||
tag_patterns = {
|
||||
"v": r"^DMARC1$",
|
||||
"p": r"^(none|quarantine|reject)$",
|
||||
"sp": r"^(none|quarantine|reject)$",
|
||||
"pct": r"^(100|[1-9]?[0-9])$",
|
||||
"rua": r"^[\w\.\-]+@[\w\.\-]+\.[a-zA-Z]{2,}$",
|
||||
"ruf": r"^[\w\.\-]+@[\w\.\-]+\.[a-zA-Z]{2,}$",
|
||||
"rf": r"^(afrf)$",
|
||||
"fo": r"^(0|1|d|s)$",
|
||||
"ri": r"^\d+$",
|
||||
"aspf": r"^(r|s)$",
|
||||
"adkim": r"^(r|s)$",
|
||||
}
|
||||
|
||||
tags = record.split(";")
|
||||
|
||||
if len(tags) < 2 or not tags[0].strip().startswith("v=DMARC1"):
|
||||
return False
|
||||
|
||||
for tag in tags:
|
||||
tag = tag.strip()
|
||||
if not tag:
|
||||
continue
|
||||
tag_key_value = tag.split("=")
|
||||
|
||||
if len(tag_key_value) != 2:
|
||||
return False
|
||||
|
||||
tag_key, tag_value = tag_key_value
|
||||
tag_key = tag_key.lower().strip()
|
||||
tag_value = tag_value.strip()
|
||||
|
||||
if tag_key in tag_patterns:
|
||||
if not re.match(tag_patterns[tag_key], tag_value):
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
215
spoofy.py
215
spoofy.py
@@ -1,100 +1,153 @@
|
||||
#! /usr/bin/env python3
|
||||
|
||||
# spoofy.py
|
||||
import argparse
|
||||
import tldextract
|
||||
import threading
|
||||
import os
|
||||
from libs import bimi, dmarc, dns, logic, spf, report
|
||||
from queue import Queue
|
||||
from modules.dns import DNS
|
||||
from modules.spf import SPF
|
||||
from modules.dmarc import DMARC
|
||||
from modules.bimi import BIMI
|
||||
from modules.spoofing import Spoofing
|
||||
from modules import report
|
||||
|
||||
print_lock = threading.Lock()
|
||||
|
||||
|
||||
def process_domain(domain, output):
|
||||
"""This function takes a list of domains and an output format (either 'xls' or 'stdout')
|
||||
as arguments. It processes each domain, collects its relevant details,
|
||||
and outputs the results to the console or an Excel file."""
|
||||
try:
|
||||
dns_server = spf_record = dmarc_record = None
|
||||
spf_all = spf_includes = p = pct = aspf = sp = fo = rua = vbimi = location = authority = None
|
||||
subdomain = bool(tldextract.extract(domain).subdomain)
|
||||
with print_lock:
|
||||
dns_server, spf_record, dmarc_record, bimi_record = dns.get_dns_server(domain)
|
||||
if spf_record:
|
||||
spf_all = spf.get_spf_all_string(spf_record)
|
||||
spf_includes = spf.get_spf_includes(domain)
|
||||
if dmarc_record:
|
||||
p, pct, aspf, sp, fo, rua = dmarc.get_dmarc_details(dmarc_record)
|
||||
if bimi_record:
|
||||
vbimi, location, authority = bimi.get_bimi_details(bimi_record)
|
||||
spoofable = logic.is_spoofable(
|
||||
domain, p, aspf, spf_record, spf_all, spf_includes, sp, pct)
|
||||
if output == "xls":
|
||||
with print_lock:
|
||||
data = [{'DOMAIN': domain,
|
||||
'SUBDOMAIN': subdomain,
|
||||
'SPF': spf_record,
|
||||
'SPF MULTIPLE ALLS': spf_all,
|
||||
'SPF TOO MANY INCLUDES': spf_includes,
|
||||
'DMARC': dmarc_record,
|
||||
'DMARC POLICY': p,
|
||||
'DMARC PCT': pct,
|
||||
'DMARC ASPF': aspf,
|
||||
'DMARC SP': sp,
|
||||
'DMARC FORENSIC REPORT': fo,
|
||||
'DMARC AGGREGATE REPORT': rua,
|
||||
'BIMI_RECORD': bimi_record,
|
||||
'BIMI_VERSION': vbimi,
|
||||
'BIMI_LOCATION': location,
|
||||
'BIMI_AUTHORITY': authority,
|
||||
'SPOOFING POSSIBLE': spoofable}]
|
||||
report.write_to_excel(data)
|
||||
else:
|
||||
with print_lock:
|
||||
report.printer(domain, subdomain, dns_server, spf_record, spf_all, spf_includes, dmarc_record, p, pct, aspf,
|
||||
sp, fo, rua, bimi_record, vbimi, location, authority, spoofable)
|
||||
except Exception as e:
|
||||
raise e
|
||||
with print_lock:
|
||||
report.output_error(
|
||||
f"Domain {domain} is offline or format cannot be interpreted.")
|
||||
def process_domain(domain):
|
||||
"""Process a domain to gather DNS, SPF, DMARC, and BIMI records, and evaluate spoofing potential."""
|
||||
dns_info = DNS(domain)
|
||||
spf = SPF(domain, dns_info.dns_server)
|
||||
dmarc = DMARC(domain, dns_info.dns_server)
|
||||
bimi_info = BIMI(domain, dns_info.dns_server)
|
||||
|
||||
spf_record = spf.spf_record
|
||||
spf_all = spf.all_mechanism
|
||||
spf_dns_query_count = spf.spf_dns_query_count
|
||||
spf_too_many_dns_queries = spf.too_many_dns_queries
|
||||
|
||||
dmarc_record = dmarc.dmarc_record
|
||||
dmarc_p = dmarc.policy
|
||||
dmarc_pct = dmarc.pct
|
||||
dmarc_aspf = dmarc.aspf
|
||||
dmarc_sp = dmarc.sp
|
||||
dmarc_fo = dmarc.fo
|
||||
dmarc_rua = dmarc.rua
|
||||
|
||||
bimi_record = bimi_info.bimi_record
|
||||
bimi_version = bimi_info.version
|
||||
bimi_location = bimi_info.location
|
||||
bimi_authority = bimi_info.authority
|
||||
|
||||
spoofing_info = Spoofing(
|
||||
domain,
|
||||
dmarc_record,
|
||||
dmarc_p,
|
||||
dmarc_aspf,
|
||||
spf_record,
|
||||
spf_all,
|
||||
spf_dns_query_count,
|
||||
dmarc_sp,
|
||||
dmarc_pct,
|
||||
)
|
||||
|
||||
domain_type = spoofing_info.domain_type
|
||||
spoofing_possible = spoofing_info.spoofing_possible
|
||||
spoofing_type = spoofing_info.spoofing_type
|
||||
|
||||
result = {
|
||||
"DOMAIN": domain,
|
||||
"DOMAIN_TYPE": domain_type,
|
||||
"DNS_SERVER": dns_info.dns_server,
|
||||
"SPF": spf_record,
|
||||
"SPF_MULTIPLE_ALLS": spf_all,
|
||||
"SPF_NUM_DNS_QUERIES": spf_dns_query_count,
|
||||
"SPF_TOO_MANY_DNS_QUERIES": spf_too_many_dns_queries,
|
||||
"DMARC": dmarc_record,
|
||||
"DMARC_POLICY": dmarc_p,
|
||||
"DMARC_PCT": dmarc_pct,
|
||||
"DMARC_ASPF": dmarc_aspf,
|
||||
"DMARC_SP": dmarc_sp,
|
||||
"DMARC_FORENSIC_REPORT": dmarc_fo,
|
||||
"DMARC_AGGREGATE_REPORT": dmarc_rua,
|
||||
"BIMI_RECORD": bimi_record,
|
||||
"BIMI_VERSION": bimi_version,
|
||||
"BIMI_LOCATION": bimi_location,
|
||||
"BIMI_AUTHORITY": bimi_authority,
|
||||
"SPOOFING_POSSIBLE": spoofing_possible,
|
||||
"SPOOFING_TYPE": spoofing_type,
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def process_domains(domains, output):
|
||||
"""
|
||||
This function is for multithreading woot woot!
|
||||
"""
|
||||
threads = []
|
||||
def worker(domain_queue, print_lock, output, results):
|
||||
"""Worker function to process domains and output results."""
|
||||
while True:
|
||||
domain = domain_queue.get()
|
||||
if domain is None:
|
||||
break
|
||||
result = process_domain(domain)
|
||||
with print_lock:
|
||||
if output == "stdout":
|
||||
report.printer(**result)
|
||||
else:
|
||||
results.append(result)
|
||||
domain_queue.task_done()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Process domains to gather DNS, SPF, DMARC, and BIMI records."
|
||||
)
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument("-d", type=str, help="Single domain to process.")
|
||||
group.add_argument(
|
||||
"-iL", type=str, help="File containing a list of domains to process."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
type=str,
|
||||
choices=["stdout", "xls"],
|
||||
default="stdout",
|
||||
help="Output format: stdout or xls (default: stdout).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-t", type=int, default=4, help="Number of threads to use (default: 4)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.d:
|
||||
domains = [args.d]
|
||||
elif args.iL:
|
||||
with open(args.iL, "r") as file:
|
||||
domains = [line.strip() for line in file]
|
||||
|
||||
domain_queue = Queue()
|
||||
results = []
|
||||
|
||||
for domain in domains:
|
||||
thread = threading.Thread(target=process_domain, args=(domain, output))
|
||||
domain_queue.put(domain)
|
||||
|
||||
threads = []
|
||||
for _ in range(min(args.t, len(domains))):
|
||||
thread = threading.Thread(
|
||||
target=worker, args=(domain_queue, print_lock, args.o, results)
|
||||
)
|
||||
thread.start()
|
||||
threads.append(thread)
|
||||
|
||||
domain_queue.join()
|
||||
|
||||
if args.o == "xls" and results:
|
||||
report.write_to_excel(results)
|
||||
print("Results written to output.xlsx")
|
||||
|
||||
for _ in range(len(threads)):
|
||||
domain_queue.put(None)
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument("-iL", type=str, required=False,
|
||||
help="Provide an input list.")
|
||||
group.add_argument("-d", type=str, required=False,
|
||||
help="Provide a single domain.")
|
||||
parser.add_argument("-o", type=str, choices=['xls', 'stdout'],
|
||||
default='stdout', help="Output format: stdout or xls (default: stdout)")
|
||||
options = parser.parse_args()
|
||||
options = parser.parse_args()
|
||||
if not any(vars(options).values()):
|
||||
parser.error(
|
||||
"No arguments provided. Usage: `spoofy.py -d [DOMAIN] -o [stdout or xls]` OR `spoofy.py -iL [DOMAIN_LIST] -o [stdout or xls]`")
|
||||
domains = []
|
||||
if options.iL:
|
||||
try:
|
||||
with open(options.iL, "r") as f:
|
||||
for line in f:
|
||||
domains.append(line.strip('\n'))
|
||||
except IOError:
|
||||
report.output_error("File doesnt exist or cannot be read.")
|
||||
if options.d:
|
||||
domains.append(options.d)
|
||||
process_domains(domains, options.o)
|
||||
main()
|
||||
|
||||
141
test.py
141
test.py
@@ -1,49 +1,134 @@
|
||||
import unittest
|
||||
from libs import logic
|
||||
from modules.spoofing import Spoofing
|
||||
|
||||
|
||||
class TestSpoofy(unittest.TestCase):
|
||||
|
||||
'''
|
||||
0: Indicates that spoofing is possible for the domain.
|
||||
1: Indicates that subdomain spoofing is possible for the domain.
|
||||
2: Indicates that organizational domain spoofing is possible for the domain.
|
||||
3: Indicates that spoofing might be possible for the domain.
|
||||
4: Indicates that spoofing might be possible (mailbox dependent) for the domain.
|
||||
5: Indicates that organizational domain spoofing may be possible for the domain.
|
||||
6: Indicates that subdomain spoofing might be possible (mailbox dependent) for the domain.
|
||||
7: Indicates that subdomain spoofing is possible, and organizational domain spoofing might be possible.
|
||||
8: Indicates that spoofing is not possible for the domain.
|
||||
'''
|
||||
|
||||
def test_spoofing_is_possible(self):
|
||||
unittest.TestCase().assertEqual(logic.is_spoofable('test_0.com', 'none', 'r', 'v=spf1 include:fake.gov', '~all', 3, None, 100), 0)
|
||||
spoofing = Spoofing(
|
||||
domain="test_0.com",
|
||||
dmarc_record="v=DMARC1; p=none;",
|
||||
p="none",
|
||||
aspf="r",
|
||||
spf_record="v=spf1 include:fake.gov",
|
||||
spf_all="~all",
|
||||
spf_dns_queries=3,
|
||||
sp=None,
|
||||
pct=100,
|
||||
)
|
||||
self.assertEqual(spoofing.spoofable, 0)
|
||||
|
||||
def test_subdomain_spoofing(self):
|
||||
unittest.TestCase().assertEqual(logic.is_spoofable("test_1.com", 'reject', None, 'v=spf1 include:fakest.domain.com', '-all', 3, 'none', None), 1)
|
||||
|
||||
spoofing = Spoofing(
|
||||
domain="test_1.com",
|
||||
dmarc_record="v=DMARC1; p=reject;",
|
||||
p="none",
|
||||
aspf=None,
|
||||
spf_record="v=spf1 include:fakest.domain.com",
|
||||
spf_all="-all",
|
||||
spf_dns_queries=3,
|
||||
sp="none",
|
||||
pct=100,
|
||||
)
|
||||
self.assertEqual(spoofing.spoofable, 1)
|
||||
|
||||
def test_organizational_domain_spoofing(self):
|
||||
unittest.TestCase().assertEqual(logic.is_spoofable('test_2.com', 'none', 'r', 'v=spf1 include:fakest.domain.com include:faker.domain.com', '-all', 2, 'reject', 100), 2)
|
||||
|
||||
spoofing = Spoofing(
|
||||
domain="test_2.com",
|
||||
dmarc_record="v=DMARC1; p=none;",
|
||||
p="none",
|
||||
aspf="r",
|
||||
spf_record="v=spf1 include:fakest.domain.com include:faker.domain.com",
|
||||
spf_all="-all",
|
||||
spf_dns_queries=2,
|
||||
sp="reject",
|
||||
pct=100,
|
||||
)
|
||||
self.assertEqual(spoofing.spoofable, 2)
|
||||
|
||||
def test_spoofing_might_be_possible(self):
|
||||
unittest.TestCase().assertEqual(logic.is_spoofable('test_3.com', 'none', None, 'v=spf1 include:fakest.domain.com', '~all', 1, 'quarantine', 90), 3)
|
||||
spoofing = Spoofing(
|
||||
domain="test_3.com",
|
||||
dmarc_record="v=DMARC1; p=none;",
|
||||
p="none",
|
||||
aspf=None,
|
||||
spf_record="v=spf1 include:fakest.domain.com",
|
||||
spf_all="~all",
|
||||
spf_dns_queries=1,
|
||||
sp="quarantine",
|
||||
pct=90,
|
||||
)
|
||||
self.assertEqual(spoofing.spoofable, 3)
|
||||
|
||||
def test_spoofing_might_be_possible_mbd(self):
|
||||
unittest.TestCase().assertEqual(logic.is_spoofable('test_4.com', 'none', None, 'v=spf1 include:fakest.domain.com', '-all', 1, None, 100), 4)
|
||||
spoofing = Spoofing(
|
||||
domain="test_4.com",
|
||||
dmarc_record="v=DMARC1; p=none;",
|
||||
p="none",
|
||||
aspf=None,
|
||||
spf_record="v=spf1 include:fakest.domain.com",
|
||||
spf_all="-all",
|
||||
spf_dns_queries=1,
|
||||
sp=None,
|
||||
pct=100,
|
||||
)
|
||||
self.assertEqual(spoofing.spoofable, 4)
|
||||
|
||||
def test_org_domain_spoofing_might_be_possible(self):
|
||||
unittest.TestCase().assertEqual(logic.is_spoofable('test_5.com', 'none', None, 'v=spf1 include:fakest.domain.com', '-all', 1, 'reject', 100), 5)
|
||||
spoofing = Spoofing(
|
||||
domain="test_5.com",
|
||||
dmarc_record="v=DMARC1; p=none;",
|
||||
p="none",
|
||||
aspf=None,
|
||||
spf_record="v=spf1 include:fakest.domain.com",
|
||||
spf_all="-all",
|
||||
spf_dns_queries=1,
|
||||
sp="reject",
|
||||
pct=100,
|
||||
)
|
||||
self.assertEqual(spoofing.spoofable, 5)
|
||||
|
||||
def test_subdomain_spoofing_might_be_possible_mbd(self):
|
||||
unittest.TestCase().assertEqual(logic.is_spoofable('test_6.com', 'reject', 'r', 'v=spf1 include:fakest.domain.com', '?all', 1, 'none', 100), 6)
|
||||
spoofing = Spoofing(
|
||||
domain="test_6.com",
|
||||
dmarc_record="v=DMARC1; p=reject;",
|
||||
p="reject",
|
||||
aspf="r",
|
||||
spf_record="v=spf1 include:fakest.domain.com",
|
||||
spf_all="?all",
|
||||
spf_dns_queries=1,
|
||||
sp="none",
|
||||
pct=100,
|
||||
)
|
||||
self.assertEqual(spoofing.spoofable, 6)
|
||||
|
||||
def test_subdomain_spoofing_and_org_spoofing_might_be_possible(self):
|
||||
unittest.TestCase().assertEqual(logic.is_spoofable('test_7.com', 'none', None, 'v=spf1 include:fakest.domain.com', '~all', 3, 'none', 100), 7)
|
||||
spoofing = Spoofing(
|
||||
domain="test_7.com",
|
||||
dmarc_record="v=DMARC1; p=none;",
|
||||
p="none",
|
||||
aspf=None,
|
||||
spf_record="v=spf1 include:fakest.domain.com",
|
||||
spf_all="~all",
|
||||
spf_dns_queries=3,
|
||||
sp="none",
|
||||
pct=100,
|
||||
)
|
||||
self.assertEqual(spoofing.spoofable, 7)
|
||||
|
||||
def test_spoofing_not_possible(self):
|
||||
unittest.TestCase().assertEqual(logic.is_spoofable('test_8.com', 'none', 's', 'v=spf1 include:fakest.domain.com', '~all', 1, 'quarantine', 100), 8)
|
||||
spoofing = Spoofing(
|
||||
domain="test_8.com",
|
||||
dmarc_record="v=DMARC1; p=none;",
|
||||
p="none",
|
||||
aspf="s",
|
||||
spf_record="v=spf1 include:domain.com",
|
||||
spf_all="-all",
|
||||
spf_dns_queries=1,
|
||||
sp="reject",
|
||||
pct=100,
|
||||
)
|
||||
self.assertEqual(spoofing.spoofable, 8)
|
||||
|
||||
def test_possible_bug_fix1(self):
|
||||
unittest.TestCase().assertEqual(logic.is_spoofable('sub.test_9.com', None, None, None, None, None, None, None), 0)
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user