\chapter{Impersonation Risk Assessment Tool}

\section{Motivation and Scope}
While previous chapters have analyzed the theoretical underpinnings of email authentication protocols (SPF, DKIM, DMARC) and the legal frameworks governing their adoption, practical risk assessment often remains fragmented. Organizations typically employ disparate tools for DNS compliance checking and separate services for brand protection or typosquatting detection. The tool developed in this thesis aims to bridge this gap by providing a unified, automated engine that correlates domain syntax anomalies with email authentication posture.

The scope of the tool is to perform a "black-box" assessment of a target domain or FQDN (Fully Qualified Domain Name). Unlike internal audit tools that require privileged access to mail servers, this system operates from an external perspective, simulating the reconnaissance phase of an attacker or the due diligence phase of a security analyst. It evaluates two primary vector categories: deceptive syntax (homoglyphs, typosquatting, and misleading subdomains) and permissive authentication (weak or missing DMARC/SPF policies), combining them into a single quantifiable risk score.


\section{Threat Model and Design Assumptions}
The system is designed based on a particular threat model, in which the attacker's goal is to pose as a legitimate brand or entity to perform BEC or credential phishing attacks. The model assumes three specific attack types:

\begin{enumerate}
\item \textbf{Lookalike Domains}: The adversary registers a domain name that looks like the legitimate domain name of the targeted entity, such as "\texttt{examp1e.com}" instead of "\texttt{example.com}", or they use Internationalized Domain Names (IDN) to perform homoglyph attacks.

\item \textbf{Subdomain Deception}: The attacker uses a legitimate, non-brand domain but crafts a misleading subdomain (e.g., \texttt{paypal.support.malicious.com}).

\item \textbf{Spoofing Capability}: The adversary attempts to send emails directly from the target domain, exploiting the absence of strict DMARC enforcement (p=none or missing records).
\end{enumerate}

The system assumes that the evaluator has no internal knowledge of the targeted entity's infrastructure. Consequently, the tool relies exclusively on public DNS records and algorithmic string analysis. It is assumed that while the presence of DKIM selectors cannot be discovered via DNS queries, the presence of common default selectors can be used as an indicator of the presence of email infrastructure.

Typical operational goals include:
\begin{itemize}
\item User deception via visually similar domains (typosquatting, homoglyphs).
\item Message delivery improvement through partial sender-authentication configuration (e.g., SPF softfail, DMARC policy p=none).
\item Brand-trust transference by embedding brand tokens in subdomains (e.g., paypal.com.attacker.tld), exploiting the fact that many users inspect only a prefix or the leftmost labels.
\end{itemize}

\begin{figure}[ht]
\centering
\resizebox{\textwidth}{!}{%
\begin{tikzpicture}[
  font=\large,
  >=Latex,
  node distance=10mm and 20mm,
  box/.style={draw, rounded corners, align=center, inner sep=6pt, text width=5.5cm},
  header/.style={draw, rounded corners, align=center, inner sep=4pt, text width=4.5cm},
]

% ---- Attacker ----
\node (head) [circle, draw, minimum size=6mm] {};
\draw (head.south) -- ++(0,-8mm) coordinate (body);
\draw ($(head.south)+(0,-2mm)$) -- ++(-8mm,-2mm);
\draw ($(head.south)+(0,-2mm)$) -- ++( 8mm,-2mm);
\draw (body) -- ++(-6mm,-8mm);
\draw (body) -- ++( 6mm,-8mm);
\node[below=9mm of body] (att) {\textbf{Attacker}};

% ---- Center ----
\node (center) [header, below=10mm of att] {Malicious / Lookalike Domain};

% ---- Left ----
\node (left) [box, below left=15mm and 25mm of center, align=left] {%
\textbf{Domain Name Manipulation}\\[2mm]
\begin{itemize}\setlength\itemsep{1mm}
\item IDN Homoglyph Attacks (e.g., using Cyrillic characters)
\item Typosquatting
\item Brand impersonation in subdomain ("paypal.evil.com")
\item Suspicious keywords
\item Mixed Unicode scripts
\end{itemize}
};

% ---- Right ----
\node (right) [box, below right=15mm and 25mm of center, align=left] {%
\textbf{Weak Email Auth Posture}\\[2mm]
\begin{itemize}\setlength\itemsep{1mm}
\item No / weak DMARC (policy=none, no reporting)
\item No / weak SPF (missing or multiple records)
\item No DKIM selectors
\item No MX records
\end{itemize}
};

% ---- Arrows ----
\draw[->] (att.south) -- (center.north);
\draw[->] (center.south) -- ++(0,-5mm) coordinate (branch);
\draw (branch) -- ++(-40mm,0) coordinate (L);
\draw (branch) -- ++( 40mm,0) coordinate (R);
\draw[->] (L) -- (left.north);
\draw[->] (R) -- (right.north);

\end{tikzpicture}
}
\caption{Threat model}
\end{figure}

Regarding the desing assumptions:
\begin{itemize}
\item DNS is the authoritative observation plane. All checks are based on DNS responses obtained via a recursive resolver.

\item Static analysis, not traffic verification. The tool does not verify that mail is actually signed with DKIM or that SPF authorization succeeds for a specific sending IP. Instead, it uses conservative heuristics (e.g., selector probing) to estimate likely posture.

\item eTLD+1 as the stable unit of analysis. The system collapses an input FQDN/URL into a registrable domain (eTLD+1) to standardize posture checks and scoring.

\item Explainability is a first-class output. The score is accompanied by reason tags to support analyst interpretation and to avoid “black-box” risk reporting.

\end{itemize}

\section{System Architecture}
\subsection{High-Level Architecture}

The tool is a modular Python application with a central controller. It is based on a producer-consumer pattern: a list of target domains comes in, gets processed by several parallel threads, and finally pluses up into structured reports.

The entry point is defined in \texttt{main.py}, which initializes:
\begin{itemize}
\item the argparse interface to handle inputs (which could be a single domain or a file with many of them), 
\item configuration parameters such as custom DNS resolvers and timeout values.
\end{itemize}
The system also includes other configuration options such as custom resolvers and timeout options. It uses a thread-safe approach when resolving DNS, so even if several threads are running concurrently, there won’t be any race conditions and socket exhaustion.

The tool is organized around a simple layered architecture:
\begin{enumerate}
\item \textbf{Input layer (CLI)}: Parses arguments (\texttt{---input, ---brands, ---dns, ---timeout, ---workers, ---format, ---dkim-probe}) and loads domain lists.

\item \textbf{Normalization \& decomposition}: Converts inputs into normalized domain strings and extracts the registrable domain (eTLD+1) plus optional subdomain components.

\item \textbf{Feature extraction}:
\begin{itemize}

\item \textbf{Syntax analyzer}: detects IDN/mixed-script/suspicious tokens/typosquatting/brand-in-subdomain.

\item \textbf{DNS posture analyzer}: checks MX, DMARC, SPF and optionally probes DKIM selectors.

\end{itemize}
\item \textbf{Scoring \& explainability}: Aggregates features into a bounded score $[0,100]$ and assigns a severity tier.

\item \textbf{Reporting}: Emits a table (stdout) or machine-readable CSV/JSON.
\end{enumerate}

\begin{figure}[ht]
\centering
\resizebox{\textwidth}{!}{%
\begin{tikzpicture}[
  font=\Large,
  box/.style={
    draw=black,
    very thick,
    rounded corners=3pt,
    fill=white,
    text=black,
    align=center,
    inner xsep=10pt,
    inner ysep=6pt
  },
  bigbox/.style={box, inner xsep=16pt, inner ysep=10pt, text width=9.5cm},
  smallbox/.style={box, text width=4.8cm},
  medbox/.style={box, text width=6.5cm},
  path/.style={draw=black, very thick},
  arrow/.style={path, -{Latex[length=3mm,width=2mm]}}
]

% --- Nodes (fixed layout) ---
\node[medbox] (norm) {Domain normalization \&\\ decomposition};

\node[smallbox, below left=20mm and 50mm of norm]  (syn) {Syntactic Analysis};
\node[smallbox, below right=20mm and 50mm of norm] (dns) {DNS Email Posture Analysis};

\node[bigbox, below=30mm of norm] (risk)
{\Large\bfseries Risk Scoring Engine 0--100\\[-1mm]\large\bfseries (LOW, MEDIUM, HIGH)};

\node[medbox, below=14mm of risk] (rep) {Reporting};

% --- Key coordinates for clean routing (NO lines through boxes) ---
% Junction point under "norm"
\coordinate (T)  at ($(norm.south)+(0,-8mm)$);

% Horizontal bar endpoints aligned with syn/dns, at the SAME y as T
\coordinate (HL) at (syn.north |- T);
\coordinate (HR) at (dns.north |- T);

% Elbows for feeding into risk (slightly outside the risk box)
\coordinate (LE) at ($(risk.west)+(-10mm,0)$);
\coordinate (RE) at ($(risk.east)+(10mm,0)$);

% --- Connections ---
% norm -> T (down arrow)
\draw[arrow] (norm.south) -- (T);

% horizontal bar (NO arrowheads)
\draw[path] (HL) -- (T) -- (HR);

% vertical drops to syn/dns (down arrows)
\draw[arrow] (HL) -- (syn.north);
\draw[arrow] (HR) -- (dns.north);

% syn -> risk (L-shape, arrow only into risk)
\draw[path]  (syn.south) -- ++(0,-10mm) coordinate (Sdown);
\draw[path]  (Sdown) -- (LE);
\draw[arrow] (LE) -- (risk.west);

% dns -> risk (L-shape, arrow only into risk)
\draw[path]  (dns.south) -- ++(0,-10mm) coordinate (Ddown);
\draw[path]  (Ddown) -- (RE);
\draw[arrow] (RE) -- (risk.east);

% risk -> reporting
\draw[arrow] (risk.south) -- (rep.north);

\end{tikzpicture}%
}
\caption{High-level control flow architecture}
\end{figure}



\subsection{Processing Pipeline}
The core processing logic is encapsulated in the \texttt{analyze\_one} function within \texttt{main.py}. For each input domain it executes:

   
\begin{itemize}

    \item \textbf{DNS Reconnaissance}: The pipeline instantiates a thread-local resolver via \texttt{get\_thread\_resolver} to avoid resolver-state sharing across threads and query MX, TXT (for SPF and DMARC), and specific DKIM records.
    
	\item \textbf{Syntax Analysis}: The domain is normalized and decomposed into its registrable part (eTLD+1) and subdomains using \texttt{analyze\_syntax(domain, brands)}. eTLD+1 is extracted through "syn.registrable\_domain" and query:
		\begin{itemize}
			\item \textbf{MX}: \texttt{query\_mx(resolver, etld1)}

			\item \textbf{DMARC TXT}: \texttt{query\_txt(resolver, f"\_dmarc.{etld1}")}

			\item \textbf{SPF TXT}: \texttt{query\_txt(resolver, etld1)}

			\item \textbf{DKIM} (optional): \texttt{probe\_dkim\_selectors(resolver, etld1, enabled=dkim\_probe)}
		\end{itemize}

    \item \textbf{Posture Analysis}: Raw DNS responses are parsed by dedicated modules to extract tags and semantic policy data.

    \item \textbf{Risk Scoring}: The scoring engine aggregates findings from all previous stages to compute a numeric risk score (0-100) and generate a list of "reason tags."

    \item \textbf{Result Object Construction}: A structured Result dataclass is populated and returned to the main thread for reporting.
\end{itemize}    
This pipeline design decouples data acquisition from analysis, allowing for independent extensibility of syntax checks or protocol parsers without refactoring the orchestration logic.


\section{Domain Syntax Analysis}
The syntax layer aims to detect domain-name deception patterns that are independent of authentication posture and frequently precede successful impersonation attempts.

\begin{figure}[ht]
\centering
\resizebox{\textwidth}{!}{%
\begin{tikzpicture}[
  font=\ttfamily\large,
  >=Latex,
  node distance=10mm and 18mm,
  box/.style={
    draw=black,
    very thick,
    rounded corners=2pt,
    fill=white,
    text=black,
    align=center,
    inner xsep=10pt,
    inner ysep=6pt
  },
  path/.style={draw=black, very thick, -{Latex[length=3mm,width=2mm]}},
  title/.style={font=\sffamily\bfseries\small}
]

% --- Nodes ---

\node[box, text width=8.6cm] (n0)
{normalize\_domain()\\ extract\_etld1\_and\_subdomain()};
\node[title, above=2mm of n0] {domain\_utils.py};

\node[box, right=22mm of n0, text width=5.0cm] (n1)
{analyze\_syntax()};
\node[title, above=2mm of n1] {syntax\_analyzer.py};

\node[box, right=30mm of n1, yshift=18mm, text width=3.9cm] (f1) {IDN / Mixed scripts};
\node[box, right=30mm of n1, yshift= 6mm, text width=3.9cm] (f2) {Suspicious tokens};
\node[box, right=30mm of n1, yshift=-6mm, text width=3.9cm] (f3) {Brand-in-subdomain};
\node[box, right=30mm of n1, yshift=-18mm, text width=3.9cm] (f4) {Typosquat distance};

\node[box, right=24mm of f2, yshift=0mm, text width=2.2cm] (out) {SyntaxInfo};

% --- Arrows ---
\draw[path] (n0) -- (n1);

\draw[path] (n1.east) .. controls +(16mm,10mm) and +(-16mm,0mm) .. (f1.west);
\draw[path] (n1.east) .. controls +(16mm, 4mm) and +(-16mm,0mm) .. (f2.west);
\draw[path] (n1.east) .. controls +(16mm,-4mm) and +(-16mm,0mm) .. (f3.west);
\draw[path] (n1.east) .. controls +(16mm,-10mm) and +(-16mm,0mm) .. (f4.west);

\draw[path] (f1.east) .. controls +(10mm,0mm) and +(-10mm,10mm) .. (out.west);
\draw[path] (f2.east) .. controls +(10mm,0mm) and +(-10mm, 4mm) .. (out.west);
\draw[path] (f3.east) .. controls +(10mm,0mm) and +(-10mm,-4mm) .. (out.west);
\draw[path] (f4.east) .. controls +(10mm,0mm) and +(-10mm,-10mm) .. (out.west);

\end{tikzpicture}%
}
\caption{Domain Analysis Pipeline}
\label{domainanalysisfunctions}
\end{figure}



\subsection{Input Normalization and eTLD+1 Extraction}
The tool starts normalizing user input in \texttt{normalize\_domain()} by stripping schemes (\texttt{http://, https://}), paths, ports, and trailing dots, then lowercasing the string. This ensures consistent DNS query names and stable tokenization. 
To distinguish between the organizational domain and the subdomain, the system employs the \texttt{extract\_etld1\_and\_subdomain} function. This utilizes the \texttt{tldextract} library to accurately separate the Public Suffix (e.g., \texttt{.co.uk, .com)} from the registrable domain. This distinction is critical because DMARC policies are typically inherited from the organizational domain (eTLD+1), whereas syntax-based deception often occurs in the subdomain.

\subsection{Internationalized Domains and Homoglyph Risk}
IDN homograph attacks represent a significant impersonation risk. Internationalized Domain Names (IDNs) enable Unicode labels, which introduces homoglyph/confusable risks when characters from different scripts resemble Latin glyphs. The tool flags IDN risk using \texttt{is\_idn()}, which detects either Punycode labels (\texttt{xn--}) or non-ASCII code points.
A \texttt{mixed\_scripts} boolean flag is then calculated by invoking \texttt{detect\_mixed\_scripts}, which inspects the Unicode character properties of the domain labels. If a label contains characters from multiple incompatible scripts (e.g., Cyrillic and Latin) it is flagged as high-risk, as this is a strong indicator of homoglyph obfuscation intended to deceive users. This operationalizes a practical subset of the Unicode security guidance \cite{unicodesec}, which explicitly treats mixed-script confusables as a key risk category.

\subsection{Typosquatting Detection}
Typosquatting is modeled as an edit-distance similarity between the analyzed domain label and a set of brand labels provided via \texttt{---brands}. The tool computes the distance either using the optional python Levenshtein package (fast path) or a pure-Python Damerau–Levenshtein implementation \texttt{damerau\_levenshtein\_py()}.
The tool extracts the second-level label (e.g., paypal from paypal.com) from the analyzed eTLD+1 and for each brand domain, it extracts the corresponding brand label.
If $ 0 < distance \leq 2$, the domain is treated as a typo candidate and stored in \texttt{typo\_matches}, sorted by minimum distance. The scoring layer then assigns a high weight to distance 1 (typical single substitution, insertion or deletion) and a moderate weight to distance 2, reflecting the higher exploitability of close variants in user perception.

\subsection{Subdomain-Based Deception Signals}
Attackers frequently embed sensitive keywords in subdomains to simulate legitimacy, for example \texttt{paypal.secure-login.attacker.com} visually foregrounds the target brand in the leftmost labels, while DNS ownership remains with \texttt{attacker.com}.
The tool analyzes this vector by tokenizing the subdomain string using a regular expression splitter \texttt{re.split(r"[\^a-z0-9]+", ...)}.
This procedure detects this only when the input actually contains a subdomain.
The resulting tokens are intersected with a predefined \texttt{SUSPICIOUS\_TOKENS} set, which includes terms like "login," "verify," "billing," and "secure."
Finally, if a brand list is provided, the tool checks if any brand name appears explicitly as a token within the subdomain, thus detecting "brand-in-subdomain" attacks, where a trusted brand is used as a prefix on a malicious domain. These kind of matches are scored aggressively because they indicate deliberate impersonation intent rather than incidental similarity.


\section{DNS and Email Authentication Posture Analysis}
The DNS posture analysis focuses on deployability for email-based impersonation and on policy signals that reduce the cost of spoofing campaigns.

\begin{figure}[htbp]
\centering
\resizebox{\textwidth}{!}{%
\begin{tikzpicture}[
  font=\ttfamily\large,
  >=Latex,
  % styles
  box/.style={
    draw=black,
    very thick,
    rounded corners=2pt,
    fill=white,
    text=black,
    align=center,
    inner xsep=7pt,
    inner ysep=5pt
  },
  outbox/.style={
    box,
    inner xsep=10pt,
    inner ysep=6pt
  },
  path/.style={draw=black, very thick, -{Latex[length=2.6mm,width=1.8mm]}},
  plain/.style={draw=black, very thick},
  title/.style={font=\sffamily\bfseries\small}
]

% --- Top nodes ---
\node[box, text width=4.0cm] (top) {analyze\_one(domain)};

\node[box, below=10mm of top, text width=7.6cm] (mid)
{normalize\_domain()\\ extract\_etld1\_and\_subdomain()};

\node[title, above=1.5mm of mid] {domain\_utils.py};

% --- Second row (DNS queries) ---
%\node[box, below left=16mm and 58mm of mid, text width=3.8cm] (mxq)
%{query\_mx()};
%\node[title, above=1.5mm of mxq] {dns\_client.py};


\node[box, below left=16mm and 4mm of mid, text width=6cm] (dmarc_txt)
{query\_txt(\_dmarc.domain)};

\node[title, above=1.5mm of dmarc_txt] {dns\_client.py};


\node[box, below right=16mm and -5mm of mid, text width=3.8cm] (spf_txt)
{query\_txt(domain)};
\node[title, above=1.5mm of spf_txt] {dns\_client.py};


\node[box, below right=16mm and 58mm of mid, text width=8cm] (dkim_txt)
{query\_txt(selector.\_domainkey.domain)};

\node[title, above=1.5mm of dkim_txt] {dns\_client.py};


% --- Third row (analysis/probe) ---
\node[box, below=36mm of mid, text width=3.2cm] (mxinfo_in)
{query\_mx()};
\node[title, above=1.5mm of mxinfo_in] {dns\_client.py};


\node[box, below=12mm of dmarc_txt, text width=4.2cm] (dmarc_an)
{analyze\_dmarc()};
\node[title, above=1.5mm of dmarc_an] {email\_posture.py};


\node[box, below=12mm of spf_txt, text width=4cm] (spf_an)
{analyze\_spf()};
\node[title, above=1.5mm of spf_an] {email\_posture.py};


\node[box, below=12mm of dkim_txt, text width=5.0cm] (dkim_probe)
{probe\_dkim\_selector()};
\node[title, above=1.5mm of dkim_probe] {email\_posture.py};

% --- Output row ---
\node[outbox, below=10mm of mxinfo_in, text width=1.6cm] (mxinfo) {MxInfo};
\node[outbox, below=10mm of dmarc_an,  text width=1.9cm] (dmarcinfo) {DmarcInfo};
\node[outbox, below=10mm of spf_an,    text width=1.6cm] (spfinfo) {SpfInfo};
\node[outbox, below=10mm of dkim_probe,text width=1.6cm] (dkiminfo) {DkimInfo};

% --- Connections ---
\draw[path] (top) -- (mid);

% fan-out from mid to dns queries (curved, like the screenshot)
\draw[path] (mid.south) .. controls +(-10mm,-6mm) and +(0,8mm) .. (mxinfo_in.north);
\draw[path] (mid.south) .. controls +(-4mm,-10mm) and +(0,8mm) .. (dmarc_txt.north);
\draw[path] (mid.south) .. controls +(4mm,-10mm)  and +(0,8mm) .. (spf_txt.north);
\draw[path] (mid.south) .. controls +(10mm,-6mm)  and +(0,8mm) .. (dkim_txt.north);

% down links to analysis/probe row
%\draw[path] (mxq) -- (mxinfo_in);
\draw[path] (dmarc_txt) -- (dmarc_an);
\draw[path] (spf_txt) -- (spf_an);
\draw[path] (dkim_txt) -- (dkim_probe);

% to outputs
\draw[path] (mxinfo_in) -- (mxinfo);
\draw[path] (dmarc_an)  -- (dmarcinfo);
\draw[path] (spf_an)    -- (spfinfo);
\draw[path] (dkim_probe)-- (dkiminfo);

% --- Outer frame + title ---
\node[draw=black, very thick, rounded corners=2pt, inner sep=7mm,
      fit=(top)(mid)(mxinfo_in)(dmarc_txt)(spf_txt)(dkim_txt)(mxinfo)(dmarcinfo)(spfinfo)(dkiminfo)] (frame) {};

\end{tikzpicture}%
}
\caption{Email Posture Analysis Pipeline}
\end{figure}


\subsection{MX Record Presence}
The presence of Mail Exchange (MX) records is the primary indicator of a domain's capability to receive email. Its presence is queried via \texttt{query\_mx(resolver, etld1)}. The absence of MX records suggests the domain is not intended for email reception, although it may still be used to send spoofed outbound mail, for this reason a small score increment is assigned when the record is not present to avoid over-interpreting this signal.

\subsection{DMARC Configuration Analysis}
DMARC analysis is handled by \texttt{analyze\_dmarc} function.

\begin{listingcaption}[htbp]
\caption{DMARC analysis function}
\label{lst:dmarcanalysis}

\begin{minted}[frame=single, linenos]{python}
def analyze_dmarc(txt_records: List[str]) -> DmarcInfo:
    info = DmarcInfo(raw_records=txt_records[:])
    dmarc_recs = [r for r in txt_records if "v=dmarc1" in r.lower()]
    if not dmarc_recs:
        info.present = False
        return info

    info.present = True
    if len(dmarc_recs) > 1:
        info.ambiguous = True

    rec = dmarc_recs[0]
    try:
        tags = parse_kv_tags(rec)
        if tags.get("v", "").upper() != "DMARC1":
            info.parse_error = "Invalid DMARC version tag"
        p = tags.get("p")
        if p:
            info.policy = p.lower()
        pct = tags.get("pct")
        if pct and pct.isdigit():
            info.pct = int(pct)
        rua = tags.get("rua")
        info.rua_present = bool(rua and "mailto:" in rua.lower())
        info.adkim = tags.get("adkim")
        info.aspf = tags.get("aspf")
    except Exception as e:
        info.parse_error = f"Exception parsing DMARC: {e}"
    return info
\end{minted}
\end{listingcaption}

The system queries the \texttt{\_dmarc} subdomain of the eTLD+1 and selects those containing \texttt{v=DMARC1}. The raw TXT records are parsed using the \texttt{parse\_kv\_tags} helper, which converts the semicolon-delimited string into a key-value dictionary.
The parser:
\begin{itemize}
\item tokenizes the record into \texttt{key=value} pairs via \texttt{parse\_kv\_tags()},
\item extracts \texttt{p, pct, rua}, and alignment-related tags (adkim, aspf),
\item marks the configuration as ambiguous when multiple DMARC records are present.
\end{itemize}
Key configuration checks include the verification of the DMARC version, policy enforcement, thus the \texttt{p} tag is extracted. A policy of \texttt{none} is flagged as a risk, whereas quarantine or reject indicates enforcement. If multiple DMARC records exist, it means it is ambiguous, rendering the policy invalid (\texttt{info.ambiguous = True}). Furthermore, it checks for the presence of \texttt{rua} tags containing \texttt{mailto:}, this indicates active monitoring, thus will not increase the risk score.

\subsection{SPF Configuration Analysis}
SPF parsing is based on detecting TXT records containing \texttt{v=spf1} and extracting the qualifier applied to the \texttt{all} mechanism.
%\begin{listingcaption}[htbp]
%\caption{SPF analysis function}
%\label{lst:spfanalysis}

\captionof{listing}{SPF analysis function}
\label{lst:spfanalysis}

\begin{minted}[frame=single, linenos]{python}
def analyze_spf(txt_records: List[str]) -> SpfInfo:
    info = SpfInfo(raw_records=txt_records[:])
    spf_recs = [r for r in txt_records if "v=spf1" in r.lower()]
    info.spf_records_only = spf_recs[:]
    if not spf_recs:
        info.present = False
        return info

    info.present = True
    if len(spf_recs) > 1:
        info.multiple = True

    rec = spf_recs[0]
    try:
        m = re.findall(r"(^|\s)([+\-~?]?)(all)(\Z|\s)", rec, flags=re.IGNORECASE)
        if m:
            qual = m[-1][1]
            info.all_qualifier = qual if qual else "+"
        else:
            info.all_qualifier = None

        if info.all_qualifier in ("~", "?", "+"):
            info.weak_all = True
    except Exception as e:
        info.parse_error = f"Exception parsing SPF: {e}"
    return info
\end{minted}

The parser here evaluates the "all" mechanism using a regex in line 15 (Listing \ref{lst:spfanalysis}). The use of \texttt{+all} or \texttt{?all} is flagged via the \texttt{weak\_all} attribute, as these authorize the entire internet to send email on behalf of the domain. Multiple SPF records are also detected, since they invalidates the SPF check \cite{rfc7208}.
This approach is intentionally conservative: it does not expand \texttt{include:} chains nor enforce DNS-lookup limits; instead, it estimates risk from the highest-level policy posture that attackers commonly exploit.

\subsection{DKIM Presence Heuristics}
Unlike SPF and DMARC, DKIM records are associated with specific selectors that are not discoverable via a standard directory. \cite{rfc6376}

\captionof{listing}{SPF analysis function}
\label{lst:dkimanalysis}
\begin{minted}[frame=single, linenos]{python}
COMMON_SELECTORS = ["default", "selector1", "selector2", "s1", "s2",
 "google", "smtp", "mail", "dkim"]


def probe_dkim_selectors(resolver, etld1: str, enabled: bool) -> DkimInfo:
    info = DkimInfo(probed=enabled)
    if not enabled:
        info.notes = "DKIM probing disabled (no email context)."
        return info

    found: List[str] = []
    for sel in COMMON_SELECTORS:
        name = f"{sel}._domainkey.{etld1}"
        txt = query_txt(resolver, name)
        if any("v=dkim1" in r.lower() for r in txt):
            found.append(sel)

    info.found_selectors = found
    if not found:
        info.notes = "No DKIM record found for common selectors."
    return info
\end{minted}

When \texttt{---dkim-probe} is enabled, it queries \texttt{<selector>.\_domainkey.<etld1>} for a small list of common selectors (e.g., default, selector1, google, mail). If any TXT record contains \texttt{v=DKIM1}, the selector is recorded as “found”. While not exhaustive, finding a record confirms that the domain has active cryptographic signing infrastructure. A negative result is explicitly not treated as proof of absence because many domains use non-standard selectors. Accordingly, the scoring assigns only a small penalty when probing is enabled but no common selectors are found.

\section{Risk Scoring Model}
The scoring workflow is represented in figure \ref{fig:risk_workflow}.
\begin{figure}[h!]
\centering
\resizebox{\textwidth}{!}{%
\begin{tikzpicture}[
    font=\ttfamily\small,
    box/.style={
        draw,
        rectangle,
        rounded corners=2pt,
        align=center,
        minimum height=8mm,
        inner xsep=8pt,
        inner ysep=6pt
    },
    		title/.style={
    		font=\sffamily\scriptsize,
    		align=center
	},
    decision/.style={
        draw,
        diamond,
        aspect=2,
        align=center,
        inner xsep=6pt,
        inner ysep=6pt
    },
    arrow/.style={->, thick}
]

% --- Top row ---
\node[box] (syntax) {SyntaxInfo};
\node[box, right=12mm of syntax] (mx) {MxInfo};
\node[box, right=12mm of mx] (dmarc) {DmarcInfo};
\node[box, right=12mm of dmarc] (spf) {SpfInfo};
\node[box, right=12mm of spf] (dkim) {DkimInfo};

% --- Scoring blocks ---
\node[box, below=18mm of dmarc] (score)
{score()\\0--100, reasons};
\node[title, above=1.5mm of score] {scoring.py};

\node[box, below=10mm of score] (level)
{level()\\ LOW / MEDIUM / HIGH};
\node[title, above=1.5mm of level] {scoring.py};



% --- Arrows from inputs to scoring ---
\draw[arrow] (syntax.south) -- (score.north);
\draw[arrow] (mx.south) -- (score.north);
\draw[arrow] (dmarc.south) -- (score.north);
\draw[arrow] (spf.south) -- (score.north);
\draw[arrow] (dkim.south) -- (score.north);

% --- Vertical flow ---
\draw[arrow] (score) -- (level);

\end{tikzpicture}
}
\caption{Risk scoring workflow}
\label{fig:risk_workflow}

\end{figure}

\subsection{Feature Weighting Strategy}
The scoring module implements a deterministic scoring algorithm that begins at 0 (low risk) and accumulates penalty points based on identified vulnerabilities, all of this is executed in the \texttt{score} function. The final score is clamped between 0 and 100.

\captionof{listing}{Scoring function}
\label{lst:scoringfunction}

\begin{minted}[frame=single, breaklines, linenos]{python}
def score(syn: SyntaxInfo, mx: MxInfo, dmarc: DmarcInfo, spf: SpfInfo, dkim: DkimInfo) -> Tuple[int, List[str]]:
    s = 0
    reasons: List[str] = []

    # Syntax
    if syn.idn:
        s += 20
        reasons.append("idn_or_punycode")
    if syn.mixed_scripts:
        s += 20
        reasons.append("mixed_scripts")
    if syn.suspicious_tokens:
        s += 10
        reasons.append("suspicious_tokens")
    if syn.brand_in_subdomain:
        s += 20
        reasons.append("brand_in_subdomain")

    if syn.typo_matches:
        best = syn.typo_matches[0]["distance"]
        if best == 1:
            s += 30
            reasons.append("typosquat_distance_1")
        elif best == 2:
            s += 15
            reasons.append("typosquat_distance_2")

    # DNS posture
    if not mx.present:
        s += 5
        reasons.append("no_mx")
	#dmarc
    if not dmarc.present:
        s += 15
        reasons.append("no_dmarc")
    else:
        if dmarc.ambiguous:
            s += 10
            reasons.append("ambiguous_dmarc_records")
        if dmarc.policy == "none":
            s += 10
            reasons.append("dmarc_policy_none")
        if dmarc.pct is not None and dmarc.pct < 100:
            s += 5
            reasons.append("dmarc_pct_below_100")
        if dmarc.rua_present is False:
            s += 5
            reasons.append("no_dmarc_reporting_rua")

    # SPF
    if not spf.present:
        s += 10
        reasons.append("no_spf")
    else:
        if spf.multiple:
            s += 15
            reasons.append("multiple_spf_records")

        dmarc_is_weak = (not dmarc.present) or (dmarc.policy == "none")

        if spf.weak_all:
            if dmarc_is_weak:
                s += 10
                reasons.append("spf_weak_all_under_weak_dmarc")
            else:
                s += 5
                reasons.append("spf_weak_all")
	# DKIM
    if dkim.probed and not dkim.found_selectors:
        s += 5
        reasons.append("dkim_not_found_common_selectors")

    return clamp(s, 0, 100), reasons
\end{minted}

The weighting strategy reflects two priorities:
\begin{enumerate}

\item \textbf{Attacker intent signals} receive higher weights because they are strong indicators of deliberate impersonation. Typosquatting distance 1 increases the score by 30 points, mixed-script IDNs and brand-in-subdomain by 20.

\item \textbf{Authentication posture weaknesses} (missing DMARC/SPF, permissive policies, ambiguous records) receive moderate weights because they increase the feasibility and success rate of spoofing campaigns but do not alone imply impersonation intent.
\end{enumerate}

The model encodes asymmetry between “configuration absent” and “configuration weak”: for example, missing DMARC increases risk more than DMARC \texttt{p=none} (15 points if absent, 10 if \texttt{p=none} policy), because an absent record removes both policy and reporting channels. This way of calculating weights reflects the thesis's focus: a domain that looks like a bank (syntax) and has no DMARC (auth) is an imminent high-severity threat.


To ensure the output is actionable, the score function returns a list of reasons alongside the integer score. Each penalty applied appends a string tag (e.g., \texttt{typosquat\_distance\_1}, \texttt{spf\_weak\_all}) to the list. This provides explainability, allowing the analyst to understand why a domain was classified as "HIGH" risk (score $\geq 60$) or "MEDIUM" risk (score $\geq 30$).

%% ---- INSERIRE SCREEN OUTPUT DA TERMINALE CON TRE ESEMPI (LOW, MED, HIGH)

\section{Parallel Scanning and Performance Considerations}
Given the inherent latency in DNS queries, the tool utilizes Python's \texttt{concurrent.futures.ThreadPoolExecutor} to parallelize domain analysis. In the command executed to run the tool, the argument \texttt{---workers} allows the user to control the thread pool size (default at 50).

\captionof{listing}{Thread-local DNS resolver}
\label{lst:threadresolver}

\begin{minted}[frame=single, breaklines, linenos]{python}
_thread_local = threading.local()
def get_thread_resolver(dns_servers, timeout):
    if not hasattr(_thread_local, "resolver"):
        _thread_local.resolver = make_resolver(dns_servers, timeout)
    return _thread_local.resolver
    
    
with ThreadPoolExecutor(max_workers=args.workers) as executor:
        futures = {executor.submit(task, d): d for d in domains}

        for future in as_completed(futures):
            domain = futures[future]
            try:
                results.append(future.result())
            except Exception as e:
                etld1, _, _ = extract_etld1_and_subdomain(normalize_domain(domain))
                results.append(
                    Result(
                        domain_input=domain,
                        etld_plus_one=etld1,
                        risk_score=0,
                        risk_level="LOW",
                        reasons=[f"error:{type(e).__name__}"],
                        syntax=SyntaxInfo(
                            input_domain=domain,
                            fqdn_provided=False,
                            registrable_domain=etld1,
                            subdomain="",
                        ),
                        mx=MxInfo(),
                        dmarc=DmarcInfo(parse_error=str(e)),
                        spf=SpfInfo(parse_error=str(e)),
                        dkim=DkimInfo(),
                        timestamp_utc=utc_timestamp(),
                    )
                )

            # progress counter
            done += 1
            if done % 10 == 0 or done == total:
                print(f"[progress] {done}/{total} domains analyzed")
\end{minted}

The \texttt{ThreadPoolExecutor} and \texttt{as\_completed()} scheduling make the DNS lookups I/O-bound, thus making Python threading effective despite the GIL. Meanwhile, \texttt{as\_completed()} enables streaming completion and progress reporting without waiting for the slowest domains. The function \texttt{get\_thread\_resolver()} stores a resolver instance inside \texttt{threading.local()}. This avoids cross-thread reuse of a resolver object, which can be problematic due to shared caches, mutable state, or non-thread-safe internals. Each worker thread thus holds its own resolver configured by \texttt{make\_resolver(nameservers, timeout)}.

The tool enables analysts to provide their own recursive DNS resolvers via the \texttt{---dns} parameter. Internally, each worker thread creates a thread-local resolver object based on the chosen nameservers and timeout parameters. This approach eliminates shared state and guarantees determinism in resolver behavior across parallel execution contexts.

Choosing a resolver can have non-trivial implications for reproducibility and measurement consistency. Recursive resolvers can have different cache behavior, DNSSEC validation, EDNS usage, and response latency. Consequently, a scan performed against one resolver might show different timeout behavior or transient NXDOMAINs compared to a scan performed against a different resolver. The tool’s ability to control this via an explicit parameter improves methodological transparency.



\section{Output and Reporting}
The reporting system supports three output modes:
\begin{itemize}
\item \textbf{Human-readable table} (\texttt{print\_table()}): emits Input | eTLD+1 | Score | Level | Reasons.
\begin{table}[h]
\begin{tabular}{|l|l|l|l|l|}
\hline
Input       & eTLD+1      & Score & Level & Reason                            \\ \hline
example.com & example.com & 5    & LOW   & no\_dmarc\_reporting\_rua \\ \hline
\end{tabular}
\caption{Table output example}
\end{table}

\begin{figure}[h]
    \centering
    \includegraphics[width=0.9\linewidth]{Figures/table_output_high.png}
    \caption{High score output example - domain with Cyrillic characters}
    \label{fig:highoutput}
\end{figure}


\item \textbf{JSON export} (\texttt{write\_json()}): serializes the complete Result structure, thus providing a more complete report.

\captionof{listing}{JSON output example}
\label{lst:jsonoutput}

\begin{minted}[frame=single, breaklines, linenos]{json}
[
  {
    "domain_input": "example1.com",
    "etld_plus_one": "example1.com",
    "risk_score": 25,
    "risk_level": "LOW",
    "reasons": [
      "no_dmarc",
      "spf_weak_all_under_weak_dmarc"
    ],
    "syntax": {
      "input_domain": "example1.com",
      "fqdn_provided": false,
      "registrable_domain": "example1.com",
      "subdomain": "",
      "idn": false,
      "mixed_scripts": false,
      "suspicious_tokens": [],
      "brand_in_subdomain": [],
      "typo_matches": []
    },
    "mx": {
      "present": true,
      "exchanges": [
        "mail.example1.com"
      ]
    },
    "dmarc": {
      "present": false,
      "raw_records": [],
      "policy": null,
      "pct": null,
      "rua_present": null,
      "adkim": null,
      "aspf": null,
      "ambiguous": false,
      "parse_error": null
    },
    "spf": {
      "present": true,
      "raw_records": [
        "v=spf1 a mx include:websitewelcome.com ~all"
      ],
      "spf_records_only": [
        "v=spf1 a mx include:websitewelcome.com ~all"
      ],
      "multiple": false,
      "all_qualifier": "~",
      "weak_all": true,
      "parse_error": null
    },
    "dkim": {
      "probed": false,
      "found_selectors": [],
      "notes": "DKIM probing disabled (no email context)."
    },
    "timestamp_utc": "2026-03-11T10:09:05Z"
  }
]
\end{minted}

\item \textbf{CSV export} (\texttt{write\_csv()}): flattens key attributes into fixed columns including syntax features (idn, \texttt{mixed\_scripts}, \texttt{suspicious\_tokens}), posture features (\texttt{dmarc\_policy}, \texttt{spf\_all\_qualifier}, \texttt{dkim\_found\_selectors}), and the reason list.



\begin{table}[H]
\centering
\begin{tabular}{|l|l|}
\hline
\texttt{domain\_input}          & \texttt{example.it}\\ \hline
\texttt{etld\_plus\_one}        & \texttt{example.it} \\ \hline
\texttt{risk\_score}            & \texttt{30} \\ \hline
\texttt{risk\_level}            & \texttt{MEDIUM} \\ \hline
\texttt{reasons}			& \texttt{no\_mx; no\_dmarc; no\_spf} \\ \hline
\texttt{idn}                    & \texttt{False} \\ \hline
\texttt{mixed\_scripts}         & \texttt{False} \\ \hline
\texttt{suspicious\_tokens}     &  \\ \hline
\texttt{brand\_in\_subdomain}   &  \\ \hline
\texttt{mx\_present}            & \texttt{False} \\ \hline
\texttt{dmarc\_present}         & \texttt{False} \\ \hline
\texttt{dmarc\_policy}          &  \\ \hline
\texttt{dmarc\_pct}             &  \\ \hline
\texttt{dmarc\_rua\_present}    &  \\ \hline
\texttt{dmarc\_ambiguous}       & \texttt{False} \\ \hline
\texttt{spf\_present}           & \texttt{False} \\ \hline
\texttt{spf\_multiple}          & \texttt{False} \\ \hline
\texttt{spf\_all\_qualifier}    &  \\ \hline
\texttt{spf\_weak\_all}         & \texttt{False} \\ \hline
\texttt{dkim\_probed}           & \texttt{False} \\ \hline
\texttt{dkim\_found\_selectors} &  \\ \hline
\texttt{timestamp\_utc}         & \texttt{2026-02-19T09:03:29Z} \\ \hline
\end{tabular}
\caption{CSV output example}
\end{table}



\end{itemize}

Additionally, the tool creates timestamped output directories using local time. This design supports forensic repeatability: reports can be stored as immutable snapshots and correlated with external incident timelines.


\section{Limitations and Future Work}
Despite the practical usefulness of the tool, it has some limitations.

\begin{enumerate}
\item \textbf{No message-level verification}: without SMTP transcripts or headers, the tool cannot validate DMARC alignment outcomes, DKIM signatures, or SPF sender authorization for specific IPs. The analysis is therefore posture-based rather than outcome-based.

\item \textbf{Heuristic DKIM detection}: selector probing may miss real DKIM deployments with non-common selectors; negative results must be interpreted cautiously.

\item \textbf{Partial SPF semantics}: the model does not expand \texttt{include:} or evaluate lookup limits and macro expansion; it approximates strength primarily via \texttt{all} qualifier and record multiplicity.

\item \textbf{IDN risk approximation}: mixed-script detection is a strong signal, but full confusable detection would ideally use UTS\#39 \cite{unicodesec} skeleton mapping and confusable tables rather than only script mixing.

\item \textbf{Brand list dependency}: typosquatting and brand-in-subdomain checks require a curated brand dataset; coverage and false positives depend on brand list quality.
\end{enumerate}

Some improvements that could be implemented in future versions are:
\begin{itemize}
\item implementing UTS \#39-style confusable skeleton comparison to strengthen homoglyph detection,
\item optional DNSSEC validation and resolver diversity to reduce resolver-dependent bias,
\item SPF include-chain evaluation with lookup-limit enforcement,
\item integration of DMARC aggregate reports (RUA) to transition from static posture to empirical outcomes,
\item enrichment hooks for SIEM pipelines (e.g., emitting JSON Lines plus correlation IDs for streaming ingestion).
\end{itemize}




\section{Summary}

This chapter presented the design and implementation of the Impersonation Risk Assessment Tool, a DNS-based domain analysis system aimed at evaluating impersonation exposure through the integration of syntactic deception indicators and email authentication posture assessment. 
Unlike commercial DMARC compliance checkers, this system differs because it integrates typosquatting detection, IDN risk analysis, subdomain deception indicators, and authentication configuration parsing under a single scoring system.

The design and implementation of the system are based on specific considerations, such as determinism, explainability, and reproducibility. This is further supported by the use of thread-local resolvers and parallel scanning with a risk score output.   On the other hand, from a methodological perspective, the system is based on the application of the theoretical ideas presented in the previous chapters, particularly the idea that impersonation risk is a result of the interplay between domain-level deception and authentication enforcement failure.

Although the system does not perform message-level validation or runtime telemetry correlation, it provides a robust foundation for static impersonation risk triage and can serve as a modular component within broader monitoring or forensic workflows.