Skip to content

Email Security Operations

Email security operations is the running defense of the attack channel that never stops: authentication (SPF, DKIM, DMARC — making it provable that mail from your domain is from you), gateway filtering (content, reputation, sandbox detonation), mailbox-level controls (banner warnings, external-sender tagging, time-of-click URL defense), and the human loop — user reports, SOC triage (BT1), and hunt-driven retro-sweeping when a campaign surfaces late. Inbound, the discipline is triage at scale: thousands of suspicious messages, each needing verdict (malicious / spam / benign-suspicious), scope (who else received it), and response (purge, block, hunt). Outbound, the discipline is anti-spoofing hygiene: DMARC enforcement so adversaries cannot wear your domain into your customers’ inboxes — because a spoofed invoice from your CFO domain is your incident even if it never touched your network. The craft’s center: email is the initial-access channel for the majority of real intrusions (T2’s emulation work starts here for a reason), and the defense that works is layered machinery — authentication that fails closed, detonation that runs before the click, triage that measures itself — not the annual awareness video.

Phishing is the cheapest reliable way into any organization, and the economics never stop: commodity credential kits rent for pocket change, and targeted teams (M12’s maldoc tradecraft, BEC operators) craft per-victim lures that no static filter catches. The costs land across three separate balances. Direct compromise: a stolen session cookie or a macro-enabled document converts to initial access, then everything downstream (BT4’s incident process) is expensive. Business email compromise: a convincing spoof of your executive or billing domain redirects one wire transfer — no malware, no indicator, pure fraud. Brand damage: your customers receive convincing phishing wearing your identity, and their trust drains regardless of whether your own perimeter was touched.

The defensive failure modes repeat across every program. Authentication gaps: DMARC at p=none (monitor-only forever) means every spoof of your domain delivers cleanly to customers and partners. Gateway monoculture faith: one vendor’s verdict treated as ground truth, when targeted lures routinely sail through any single engine on first sight. Triage debt: user-reported phishing sitting in a queue for days — each unexamined report is an unmeasured incident. No retro-hunt: when intel (BT10) surfaces a new campaign IOC, nobody sweeps the last 30 days of mail for it, so the message that arrived before the rule is still sitting in fifty inboxes. And banner fatigue: warnings so frequent nobody reads them, so the one dangerous external email looks identical to the forty benign ones.

attacker mail
|
v
+-----------------+ fail +------------------+
| MX / GATEWAY |------------>| QUARANTINE |
| SPF, DKIM, DMARC| | (quarantine / |
| reputation, AV, | | reject at SMTP) |
| sandbox detona- | +------------------+
| tion of attach/URL
+--------+--------+
| pass (with verdict + banners)
v
+-----------------+ user clicks "report phish"
| MAILBOX |--------------------------+
| banners, external tag, |
| time-of-click URL rewrite |
+--------+-----------------------------------+
| delivery v
v +------------------+
+-----------------+ | SOC TRIAGE (BT1) |
| victim / user | | verdict + scope |
| clicks / replies| | purge + block |
+-----------------+ +--------+---------+
(detector of last resort) |
v
+--------------------------+
| RETRO-HUNT + FEEDBACK |
| sweep 30 days for IOCs, |
| tune rules (BT9), report |
+--------------------------+

Authentication is the floor. SPF validates the envelope sender’s IP against the domain’s DNS record; DKIM validates a cryptographic signature over headers and body (surviving forwarding, which SPF does not); DMARC ties them to the visible From: domain — alignment — and tells receivers what to do on failure via policy: p=none (deliver, report), p=quarantine, p=reject. DMARC aggregate reports (rua) land daily as XML and tell you exactly who is sending as your domain and whether it passes — dmarchealth (below) reads them. The rollout discipline matters more than the record: monitor, analyze, gradually tighten (sp= for subdomains, pct= for partial enforcement), reject.ARC preserves authentication verdicts across legitimate forwarders, and MTA-STS plus TLS-RPT harden the transport layer itself.

Gateway filtering layers content rules, sender reputation, attachment detonation (run the macro in a sandbox, watch what it does — M12’s analysis applied pre-delivery), and URL rewriting with time-of-click checks. Every engine has first-seen misses against targeted lures, which is why verdicts from multiple engines, plus your own retro-hunt capability, matter more than any single vendor’s percentage.

The human loop is a detection instrument, not a training certificate: the report-phish button must route straight into SOC triage with full headers, and response time is a first-class metric. Retro-hunting closes the timing gap — detection rules improve after delivery, so the operational question is always “who already has it”: sweep the last 30 days of message tracking for the campaign’s sender, subject cluster, and attachment hash, then purge with soft-delete across all mailboxes (BT1’s scope-then-act discipline).

Deep dive: why DMARC rollout takes months and why you must not rush it

Rejecting on day one breaks your own legitimate mail: marketing platforms, ticket systems, CRM senders, and departmental SMTP relays all send as your domain, and until each one signs with DKIM aligned to your From: domain (or is enumerated in SPF within the 10-lookup limit), p=reject bounces real business email into silence. The aggregate reports are the map: they enumerate every sending source on earth that uses your domain, including the adversary’s. The safe sequence is p=none for at least a full month (capture a complete sending-source inventory and fix the legit senders), then p=quarantine with pct=100 after zero unexplained failures for two consecutive weeks, then p=reject — monitoring the quarantine reports for stragglers. Programs that skip the monitor phase learn about their own billing system’s un-signed relay from an angry finance team, not from the report they never opened.

TechniqueWhat it doesWhere it failsNotes
SPF/DKIM/DMARC enforcementSpoofing provably fails closedUn-enumerated legit sendersMonitor → tighten → reject
Lookalike domain watchRegistrations near your brand flaggedNew TLDs, homoglyph tricksDefensive registration too
Attachment detonationRuns payload in sandbox pre-deliveryNovel/latent executionM12 analysis, pre-delivery
URL rewrite + time-of-clickVerdict re-checked at click timeShortened/redirect chainsPairs with proxy
External-sender bannerVisible cue on outside mailBanner fatigueKeep it sparse, consistent
Display-name spoof detectionFlags “CEO random@domain” patternsPersonal aliasesphishtriage rule
User report pipelineButton → SOC with full headersQueue latency kills valueMeasure response time
Retro-hunt on intelSweep history for new campaign IOCsRetention limits30-day sweep standard
BEC process controlsPayment-change verification out-of-bandUrgency override by execsProcess control, not tech
DMARC report miningWho sends as you, pass/fail ratesReports unopened for monthsdmarchealth’s job

phishtriage — an .eml triage engine: parses raw headers, evaluates SPF/DKIM/DMARC results from Authentication-Results, flags display-name spoofing and lookalike domains against your domain list, extracts URLs and attachment metadata, and emits a verdict score with reasons. Pure stdlib — the email module does the RFC822 heavy lifting.

#!/usr/bin/env python3
"""phishtriage — triage a raw .eml: auth results, spoof cues, URLs, attachments.
Usage: python3 phishtriage.py suspect.eml --domains mycorp.com,mycorp.co.uk
Verdict bands: score >= 4 malicious, 2-3 suspicious, < 2 likely-benign.
"""
import argparse
import csv
import difflib
import re
import sys
from email import policy
from email.parser import BytesParser
from pathlib import Path
URL_RE = re.compile(r"https?://[^\s\"'<>)]+", re.I)
DISPLAY_SPOOF = re.compile(
r"(ceo|cfo|cto|director|president|finance|hr|payroll|invoice|billing|support)",
re.I)
ATTACH_RISK = {".exe": 3, ".js": 3, ".vbs": 3, ".hta": 3, ".scr": 3,
".iso": 2, ".img": 2, ".lnk": 2, ".zip": 1, ".7z": 1,
".docm": 2, ".xlsm": 2, ".pdf": 0}
def auth_results(msg):
"""Pull SPF/DKIM/DMARC verdicts from Authentication-Results headers."""
out = {"spf": None, "dkim": None, "dmarc": None}
for header in msg.get_all("Authentication-Results", []):
low = str(header).lower()
for mech in out:
m = re.search(rf"\b{mech}=(pass|fail|softfail|neutral|none|temperror|permerror)", low)
if m and out[mech] is None:
out[mech] = m.group(1)
return out
def core_label(dom):
return dom.split(".")[0]
def closest_domain(dom, my_domains, threshold=0.80):
"""Lookalike check: similarity on the core label, plus prefix-containment
(mycorp-login contains mycorp) which ratio alone under-scores."""
c = core_label(dom)
best, ratio = None, 0.0
for mine in my_domains:
mc = core_label(mine)
r = difflib.SequenceMatcher(None, c, mc).ratio()
if (c.startswith(mc) or mc.startswith(c)) and min(len(c), len(mc)) >= 4:
r = max(r, 0.95)
if r > ratio:
best, ratio = mine, r
return (best, round(ratio, 3)) if ratio >= threshold else (None, 0.0)
def main():
ap = argparse.ArgumentParser(description="triage one .eml file")
ap.add_argument("eml", type=Path)
ap.add_argument("--domains", required=True,
help="comma-separated list of your org's domains")
args = ap.parse_args()
if not args.eml.exists():
sys.exit(f"fail-loud: file not found: {args.eml}")
raw = args.eml.read_bytes()
msg = BytesParser(policy=policy.default).parsebytes(raw)
sender = str(msg.get("From", "") or "")
addr_match = re.search(r"<([^>]+)>", sender)
from_addr = (addr_match.group(1) if addr_match else sender).strip().lower()
from_domain = from_addr.rsplit("@", 1)[-1] if "@" in from_addr else ""
if not from_domain:
sys.exit("fail-loud: cannot parse From: address")
display = re.sub(r"<[^>]+>", "", sender).strip().strip('"')
my_domains = [d.strip().lower() for d in args.domains.split(",") if d.strip()]
auth = auth_results(msg)
reasons, score = [], 0
# authentication signals
if auth["dmarc"] == "fail":
score += 2
reasons.append("DMARC fail (spoof-eligible)")
if auth["spf"] == "fail":
score += 1
reasons.append("SPF fail")
if auth["dkim"] == "fail":
score += 1
reasons.append("DKIM fail")
# display-name impersonation: role word in display, sender elsewhere
if display and DISPLAY_SPOOF.search(display) and from_domain not in my_domains:
score += 2
reasons.append(f"display-name role spoof: {display[:40]!r} at {from_domain}")
# lookalike domain vs. our estate
look, ratio = closest_domain(from_domain, my_domains)
if look:
score += 3
reasons.append(f"lookalike domain: {from_domain} ~ {look} (ratio {ratio})")
# reply-to divergence
reply_to = str(msg.get("Reply-To", "") or "").strip().lower()
if reply_to and from_addr and reply_to.rsplit("@", 1)[-1] not in my_domains \
and reply_to != from_addr:
score += 1
reasons.append(f"reply-to diverges: {reply_to}")
# urls and attachments
body = ""
if msg.is_multipart():
for part in msg.walk():
if part.get_content_type() == "text/plain" and not body:
try:
body = part.get_content()
except Exception:
body = ""
else:
try:
body = msg.get_content()
except Exception:
body = ""
urls = sorted(set(u.rstrip(".,;") for u in URL_RE.findall(body)))
url_domains = sorted({u.split("//", 1)[-1].split("/", 1)[0].lower()
for u in urls})
attach = []
for part in msg.walk():
fn = part.get_filename()
if fn:
ext = Path(fn).suffix.lower()
attach.append((fn, ATTACH_RISK.get(ext, 0)))
score += ATTACH_RISK.get(ext, 0)
if ATTACH_RISK.get(ext, 0) >= 2:
reasons.append(f"risky attachment: {fn}")
verdict = ("malicious" if score >= 4 else
"suspicious" if score >= 2 else "likely-benign")
print(f"from : {from_addr} (display: {display[:40]!r})")
print(f"auth : spf={auth['spf']} dkim={auth['dkim']} dmarc={auth['dmarc']}")
print(f"urls : {len(urls)} -> {url_domains[:6]}")
print(f"attachment: {attach if attach else 'none'}")
print(f"score : {score} verdict: {verdict}")
for r in reasons:
print(f" - {r}")
with (args.eml.parent / "triage_log.csv").open("a", newline="",
encoding="utf-8") as fh:
w = csv.writer(fh)
w.writerow([args.eml.name, from_addr, score, verdict,
auth["spf"], auth["dkim"], auth["dmarc"],
"|".join(url_domains), "|".join(f"{n}:{s}" for n, s in attach)])
if __name__ == "__main__":
main()

auth_results() (lines 26-35) scrapes every Authentication-Results header for the three mechanism verdicts, first-wins per mechanism — gateways prepend their own header, so the topmost (their) verdict is what counts. DISPLAY_SPOOF (lines 22-23) is the BEC workhorse: role words (ceo, payroll, invoice) in the display name combined with a non-corporate sending domain is the pattern behind most executive-impersonation mail, worth 2 points (lines 92-95).

closest_domain() (lines 38-56) works on the core label (everything before the first dot) with two rules: a SequenceMatcher similarity at a 0.80 threshold, and a prefix-containment rule — mycorp-login contains mycorp, which a naive full-string ratio under-scores (0.77) but which is exactly how commodity phishing domains are built; containment bumps the score to 0.95 and trips the +3 lookalike rule (lines 106-110), the single most predictive signal in commodity credential phishing. Reply-to divergence (lines 113-116) catches the “replies go to a Gmail” BEC pattern. URL extraction (lines 118-133) pulls both bodies and URLs, normalizes trailing punctuation, and derives the domain set the analyst will eyeball; attachment scoring (lines 135-143) uses the extension risk table (ISO/IMG and macro-enabled Office carry real weight because both bypass simple mark-of-the-web checks). The verdict bands (line 146) are deliberately asymmetric — 4+ is malicious because stacked independent signals rarely co-occur in benign mail — and every point leaves a reason string (lines 148-150) so the analyst’s job shifts from analysis to confirmation. The append-only triage_log.csv (lines 158-165) is the triage queue’s audit trail.

A credential-phish reaches a mailbox through exactly this gauntlet. The adversary registers mycorp-login.com (registered last week, no reputation), sends from a fresh VPS with no SPF, and the gateway’s verdict is some engine says spam but first-seen means others abstain — the message delivers with an external banner, display name “IT Helpdesk”, body URL pointing at a cloned SSO page. If the user reports it, phishtriage produces: DMARC fail (+2), display-name role spoof (+2), lookalike domain (+3) — score 7, malicious, with the domain list the analyst needs. Response is then mechanical: message tracking pulls every recipient of that sender/subject cluster, soft-delete purges across mailboxes, the sender domain and URL join the mail gateway’s blocklist, and the SSO page’s host goes to BT10 as a sighting.

The retro-hunt is what catches the timing gap: three days later BT10 flags the campaign’s URL infrastructure, and the 30-day message-tracking sweep finds the same sender delivered 40 more messages with a different lure body — all purged, all logged, and the miss (40 inboxes for 3 days) becomes a tuning datapoint for the gateway rules (BT9). Outbound, the mirror-image incident is the spoofed invoice: a customer calls asking about your “billing update” email, and dmarchealth shows the aggregate reports have been carrying mycorp-billing.net as a failing source for two weeks — visibility only DMARC reporting provides, and the reason outbound enforcement is an inbound-defense tool.

  1. Recon — adversary harvests exec names, org domains, and a recent business event to anchor the lure
  2. Registration — lookalike domain or aged spoofed sender; SPF/DKIM/DMARC all failing by design
  3. Delivery — targeted lure passes first-seen gateway verdicts; lands in inboxes with banner only
  4. Engagement — user clicks cloned-SSO URL or enables the macro (M12 payload chain)
  5. Capture — credentials or execution; reply-to divergence silently redirects BEC threads
  6. Detection — user report or banner-driven alert routes to SOC; phishtriage scores and scopes
  7. Response — tracking sweep → purge across mailboxes → blocklist → BT10 sighting
  8. Hardening — retro-hunt closes the timing gap; rules tune (BT9); DMARC policy tightens

DMARC enforcement is only as good as your reading of the reports it generates. dmarchealth parses DMARC aggregate report XML (the rua inbox collects them daily, gzipped, one per receiving organization) and answers the operational question: who is sending as our domains, from where, and does it pass?

Aggregate reports follow the published feedback schema — <record> with <row> (source IP, count, policy evaluated) and <auth_results> (SPF/DKIM per source). The tool aggregates by (source IP → SPF result, DKIM result, aligned domain) rather than dumping rows, because the analyst’s question is per-sender health, not per-mail volume. Sources that pass SPF but fail DKIM alignment are flagged as “forwarder-style” (benign pattern), while sources failing both are the spoofing candidates. XML is parsed with stdlib xml.etree; gzip and plain XML both accepted; fail-loud on a schema-less file so a misrouted report never silently zeroes your stats.

#!/usr/bin/env python3
"""dmarchealth — summarize DMARC aggregate reports (rua XML) into a sender table.
Usage: python3 dmarchealth.py reports/*.xml --domain mycorp.com
(accepts .xml and .xml.gz as produced by receivers' rua mail)
"""
import argparse
import glob
import gzip
import sys
import xml.etree.ElementTree as ET
from collections import defaultdict
from pathlib import Path
def open_report(path: Path):
if path.suffix == ".gz":
return gzip.open(path, "rb")
return path.open("rb")
def parse_report(path: Path):
with open_report(path) as fh:
try:
root = ET.parse(fh).getroot()
except ET.ParseError as e:
sys.exit(f"fail-loud: {path.name}: not valid DMARC XML ({e})")
ns = ""
if root.tag.startswith("{"):
ns = root.tag.split("}")[0] + "}"
if not root.tag.endswith("feedback"):
sys.exit(f"fail-loud: {path.name}: root element is not <feedback>")
org = root.findtext(f"{ns}report_metadata/{ns}org_name") or "unknown"
records = []
for rec in root.findall(f"{ns}record"):
row = rec.find(f"{ns}row")
ip = row.findtext(f"{ns}source_ip") or "?"
count = int(row.findtext(f"{ns}count") or 0)
policy = row.find(f"{ns}policy_evaluated")
disp = policy.findtext(f"{ns}disposition") or "none"
dkim_pass = policy.findtext(f"{ns}dkim") or "fail"
spf_pass = policy.findtext(f"{ns}spf") or "fail"
hdr_from = (rec.findtext(f"{ns}identities/{ns}header_from")
or (rec.findtext(f"{ns}identity") or "")).lower()
records.append({"org": org, "ip": ip, "count": count,
"disposition": disp, "dkim": dkim_pass,
"spf": spf_pass, "header_from": hdr_from})
if not records:
sys.exit(f"fail-loud: {path.name}: zero <record> entries")
return records
def main():
ap = argparse.ArgumentParser(description="DMARC aggregate report health")
ap.add_argument("patterns", nargs="+", help="report files or globs")
ap.add_argument("--domain", required=True, help="your organizational domain")
args = ap.parse_args()
files = []
for pat in args.patterns:
matched = sorted(glob.glob(pat))
if not matched:
sys.exit(f"fail-loud: pattern matched nothing: {pat}")
files.extend(matched)
senders = defaultdict(lambda: {"count": 0, "orgs": set(),
"spf_pass": 0, "dkim_pass": 0,
"dkim_domains": set(), "dispositions": set()})
total = 0
for f in files:
for rec in parse_report(Path(f)):
if rec["header_from"] and args.domain not in rec["header_from"]:
continue # report covers other org domains
key = (rec["header_from"], rec["ip"])
s = senders[key]
s["count"] += rec["count"]
s["orgs"].add(rec["org"])
s["dispositions"].add(rec["disposition"])
if rec["spf"] == "pass":
s["spf_pass"] += rec["count"]
if rec["dkim"] == "pass":
s["dkim_pass"] += rec["count"]
total += rec["count"]
if total == 0:
sys.exit(f"fail-loud: no records for domain {args.domain!r} in input")
print(f"{'header_from':<28}{'source_ip':<18}{'count':>8} "
f"{'spf':>5} {'dkim':>5} class")
rows = sorted(senders.items(), key=lambda kv: -kv[1]["count"])
for (hf, ip), s in rows:
spf_r = s["spf_pass"] / s["count"]
dkim_r = s["dkim_pass"] / s["count"]
if spf_r < 0.1 and dkim_r < 0.1:
klass = "SPOOF-CANDIDATE"
elif dkim_r < 0.1 and spf_r >= 0.9:
klass = "forwarder-style"
elif spf_r >= 0.9 and dkim_r >= 0.9:
klass = "aligned/healthy"
else:
klass = "mixed/needs-work"
print(f"{hf:<28}{ip:<18}{s['count']:>8} "
f"{spf_r:>5.0%} {dkim_r:>5.0%} {klass}")
print(f"\ntotal messages covered: {total} from {len(files)} report file(s)")
print("action: reject-eligible = aligned/healthy only; investigate SPOOF-CANDIDATE IPs")
if __name__ == "__main__":
main()

parse_report() (lines 19-46) handles the two real-world wrinkles: gzip wrapping and namespace variation — the namespace prefix is derived from the root tag rather than hardcoded, because receivers ship slightly different (but schema-equivalent) XML. The <feedback> root check is the fail-loud gate against a misplaced ordinary email attachment. main()’s aggregation key is (header_from, source_ip) (line 72) — exactly the pair the rollout decision depends on: a header_from that matches your domain plus an IP with 100% failing results is the adversary; the same header_from from your marketing platform’s IPs with aligned DKIM is rollout-blocking until fixed. The three-way classification (lines 92-101) maps directly to the DMARC rollout sequence — “aligned/healthy” senders are the ones that survive p=reject, “forwarder-style” explains SPF-pass/DKIM-fail legitimate relays, and “SPOOF-CANDIDATE” is the incident queue. The --domain filter (lines 74-75) keeps multi-domain reports from polluting one domain’s health table.

collect a week of rua attachments (they arrive as .xml.gz attachments)

Section titled “collect a week of rua attachments (they arrive as .xml.gz attachments)”

$ ls reports/ | head -3 google.com!mycorp.com!1696118400!1696204800.xml.gz outlook.com!mycorp.com!1696118400!1696204800.xml.gz yahoo.com!mycorp.com!1696118400!1696204800.xml.gz

$ python3 dmarchealth.py “reports/*.xml.gz” —domain mycorp.com header_from source_ip count spf dkim class mycorp.com 198.51.100.23 41280 100% 100% aligned/healthy mycorp.com 203.0.113.77 3900 92% 98% mixed/needs-work mycorp.com 192.0.2.66 840 0% 0% SPOOF-CANDIDATE

total messages covered: 46028 from 21 report file(s)

the SPOOF-CANDIDATE IP feeds a blocklist entry and a BT10 sighting;

Section titled “the SPOOF-CANDIDATE IP feeds a blocklist entry and a BT10 sighting;”

the mixed/needs-work sender (billing platform) blocks p=reject until fixed

Section titled “the mixed/needs-work sender (billing platform) blocks p=reject until fixed”

The tool’s output drives policy, but the same data feeds detection: any IP classed SPOOF-CANDIDATE that also appears in your inbound gateway logs is attempt evidence worth a BT10 sighting and possibly takedown action through your registrar contacts. Operationally, watch the trust-shifting failure: a source that moves from aligned/healthy to mixed (DKIM decay after a platform migration) is rollout-blocking until repaired — dmarchealth run weekly makes that drift visible before finance escalates a bounced invoice. Pair the weekly run with a lookalike-domain watch on certificate-transparency logs and registrar feeds, since the adversary’s spoof infrastructure shows up there before it shows up in your reports.

  1. Ingest the rua mailbox directly (IMAP) and auto-process daily, tracking per-sender health trends week over week.
  2. Add DKIM domain extraction from <auth_results> to distinguish your selectors from third-party ones inside aligned mail.
  3. Correlate SPOOF-CANDIDATE IPs with passive DNS and WHOIS age to auto-prioritize takedown requests.
  4. Emit a rollout-readiness verdict (blockers remaining per sender class) to pace the none → quarantine → reject sequence.
  5. Support SMTP TLS report (TLS-RPT) JSON in the same run for transport-layer health alongside identity health.
SignalSourceIDNotes
Report-phish volume & latencyTriage pipelineper weekHuman sensor health
Gateway first-seen missesGateway telemetryper campaignRetro-hunt driver
Macro process spawnSysmonEID 1winword/excel → child shell
Macro script blockWindows Security4104Dropper PowerShell
Post-click C2 callbackSysmonEID 3From phished workstation
Credential-page DNSSysmonEID 22Lookalike domain resolves
DMARC spoof candidatesdmarchealthweeklyOutbound identity attacks
Campaign purge scopeMessage trackingper incident30-day sweep coverage
Triage log coveragetriage_log.csvper reportEvery report resolved
Rule regressionBT9 gatesper deployLure-variant resilience

Adversaries engineer around every layer above. Authentication is abused, not bypassed: commodity kits send from their own registered lookalike domain (mycorp-login.com passes SPF/DKIM/DMARC of itself) — authentication proves the sender owns that domain, not that it’s yours; only lookalike detection and user-visible cues cover the gap. Display-name spoofing needs no domain at all: “M&A Steering Committee” as display name over a fresh Gmail works because busy executives read names, not addresses. Thread hijacking rides a real compromised correspondence — the reply inherits legitimacy no filter questions, and reply-to divergence is the only tell. Attachment formats rotate ahead of detonation coverage: ISO/IMG containers, OneNote, password-protected archives with the password in the body, and now cloud-hosted lures (a link to a real SharePoint page that links to the fake SSO page) — each layer looks legitimate in isolation. Time-of-click evasion uses short-lived redirect chains and geo/user-agent gates so the sandbox sees a 404 while the victim’s browser sees the lure. And rate discipline: two targeted messages to two executives will never trip volume thresholds — targeted phishing is designed to be statistically invisible.

⚠️ What caught me: we enforced DMARC correctly, banners were on, gateway was healthy — and a finance director still wired €180k against a “supplier bank change” thread where every message was authenticated, because the adversary had replied inside a months-long real correspondence stolen from the supplier’s own compromised mailbox. Every technical control reported green; the only anomaly was reply-to divergence and urgency language. That incident bought us the out-of-band verification control for payment changes — process, not technology — and taught me that email security’s last mile is never a filter verdict: it is the business process the filter cannot see.

Stand up the full loop in a lab. Authentication: add SPF, DKIM, and DMARC (p=none with rua= to a local mailbox) records for a lab domain on a private DNS server; send legitimate mail from a test relay and a “spoof” from another host; observe aggregate reports accumulate. Triage: collect or hand-craft three .eml samples — a real newsletter, a display-name spoof, a lookalike-domain credential lure — and run phishtriage on each; confirm the verdict bands separate them. Reports: generate or obtain sample aggregate XML (the spec’s examples or your own rua capture) and run dmarchealth; confirm the spoof source classes as SPOOF-CANDIDATE. Response drill: simulate the purge decision for the lure — who receives it, what’s the scope query, what’s the blocklist change — and time yourself. Debrief: which signal caught the lure that banners alone would have missed, and what does that imply for banner fatigue?

  • Technical: Walk SPF, DKIM, and DMARC as three layers — what does each prove, what defeats each, and why is DMARC alignment the one that matters for spoofing?
  • Technical: Why does a lookalike domain (mycorp-login.com) pass all three authentication checks, and which compensating controls catch it?
  • Scenario: You must move your domain from p=none to p=reject this quarter. Walk the rollout: what do the aggregate reports tell you, and what breaks if you rush it?
  • Scenario: A finance director reports a payment-change email that passed SPF, DKIM, and DMARC. How is that possible, and what do you do in the next hour?
  • Conceptual: Why is the report-phish button a detection instrument rather than a training metric, and what two numbers measure its value?
  • Deep-dive: Design email security operations for a 5,000-mailbox org: authentication posture, gateway layering, triage pipeline with staffing, retro-hunt capability, and the metrics you’d review monthly.
  • Practical: User-reported phish latency is 48 hours and the SOC wants headcount. Before hiring, which two pipeline improvements do you try, and how do you measure them?

T2 (Initial Access Emulation) · M12 (Script and Macro Malware Analysis) · BT1 (SOC Operations & Alert Triage) · BT4 (Incident Response Process) · BT9 (Detection Engineering & Sigma)