.github/skills/email-threat-posture/SKILL.md
Use this skill when asked to generate an email threat protection report, assess email security posture, or analyze email-based threats across the organization. Triggers on keywords like "email threat report", "email security posture", "email protection dashboard", "phishing report", "email threat summary", "MDO report", "Defender for Office 365 report", "email security assessment", "ZAP effectiveness", "Safe Links report", "email authentication report", "DMARC report", "spam report", "email volume report", or when asked to provide C-level visibility into email threat protection status. This skill queries EmailEvents, EmailPostDeliveryEvents, UrlClickEvents, and EmailAttachmentInfo tables in Advanced Hunting to produce a comprehensive email security posture assessment covering inbound mail flow, threat composition, phishing detection, email authentication (DMARC/DKIM/SPF), post-delivery remediation (ZAP), Safe Links click protection, attachment analysis, detection method effectiveness, and delivery disposition. Supports inline chat, markdown file, and SVG dashboard output.
npx skillsauth add scstelz/security-investigator email-threat-postureInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill generates an Email Threat Protection Posture Report using Microsoft Defender for Office 365 (MDO) telemetry available through Advanced Hunting. It provides C-level visibility into how effectively the organization's email security stack is detecting, blocking, and remediating email-based threats.
What this skill covers:
| Domain | Key Questions Answered | |--------|----------------------| | 📬 Mail Flow Overview | How many inbound emails? What's the daily trend? Who are the top senders? | | 🛡️ Threat Composition | How many phishing, spam, and malware threats were detected? | | 🎯 Phishing Protection | How many phishing emails were blocked vs delivered? Who are the most targeted users? | | 🔐 Email Authentication | What are the DMARC/DKIM/SPF/CompAuth pass rates? Which domains fail authentication? | | 🧹 Post-Delivery Remediation | How effective is ZAP? How many remediations succeeded vs failed? | | 🔗 Safe Links Protection | How many URL clicks were scanned? Were any phishing clicks allowed through? | | 📎 Attachment Analysis | What attachment types are flowing through email? Were any malicious? | | 📊 Detection Methods | What detection technologies are catching threats (URL detonation, fingerprinting, etc.)? | | 📦 Delivery Disposition | Where do emails end up — inbox, junk, quarantine, blocked? | | 🚨 MDO Incidents | How many security incidents were generated by Defender for Office? What severity, status, and types? |
Data sources: EmailEvents, EmailPostDeliveryEvents, UrlClickEvents, EmailAttachmentInfo, SecurityAlert, SecurityIncident (Advanced Hunting)
References:
MANDATORY: When generating reports, copy URLs verbatim from this registry. NEVER construct, guess, or paraphrase a URL.
| Label | Canonical URL |
|-------|---------------|
| DOCS_EMAILEVENTS | https://learn.microsoft.com/en-us/defender-xdr/advanced-hunting-emailevents-table |
| DOCS_EMAILPOSTDELIVERY | https://learn.microsoft.com/en-us/defender-xdr/advanced-hunting-emailpostdeliveryevents-table |
| DOCS_URLCLICKEVENTS | https://learn.microsoft.com/en-us/defender-xdr/advanced-hunting-urlclickevents-table |
| DOCS_EMAILATTACHMENTINFO | https://learn.microsoft.com/en-us/defender-xdr/advanced-hunting-emailattachmentinfo-table |
| DOCS_MDO_EFFICACY | https://learn.microsoft.com/en-us/defender-office-365/reports-mdo-email-collaboration-dashboard#appendix-advanced-hunting-efficacy-query-in-defender-for-office-365-plan-2 |
| DOCS_MDO_OVERVIEW | https://learn.microsoft.com/en-us/defender-office-365/mdo-about |
| DOCS_SECURITY_ALERT | https://learn.microsoft.com/en-us/azure/sentinel/data-connectors/microsoft-sentinel-security-alert |
| DOCS_ZAP | https://learn.microsoft.com/en-us/defender-office-365/zero-hour-auto-purge |
| DOCS_SAFE_LINKS | https://learn.microsoft.com/en-us/defender-office-365/safe-links-about |
Use RunAdvancedHuntingQuery by default — EmailEvents and related tables are XDR-native tables available in Advanced Hunting. Use Timestamp as the datetime column. If a query fails in AH, fall back to Sentinel Data Lake (query_lake) using TimeGenerated.
Default lookback: 7 days — Unless the user specifies a different period. This provides a meaningful weekly snapshot for executive reporting while staying within AH's 30-day retention.
ASK the user for output format before generating the report:
reports/email-threat-posture/)⛔ MANDATORY: Evidence-based analysis only — Report ONLY what query results show. Use the explicit absence pattern (✅ No [finding] detected) when queries return 0 results. Never fabricate data.
Run queries in parallel batches where possible — Phase 1 queries (Q1–Q4) are independent. Phase 2 queries (Q5–Q8) are independent. Phase 3 queries (Q9–Q12) are independent.
PII handling — Do NOT include recipient email addresses in inline reports or markdown files. Aggregate by domain or use anonymized references (e.g., "2 users in the contoso.com domain"). Top sender domains from external sources are acceptable.
Percentages must be grounded — Always show both the percentage AND the raw count (e.g., "99.8% clean (5,851 of 5,864)").
The Email Protection Score is a composite posture indicator summarizing the effectiveness of email security controls. Higher scores indicate stronger protection (inverse of a risk score).
$$ \text{EmailProtectionScore} = \sum_{i} \text{DimensionScore}_i $$
Each dimension contributes 0–20 points to a maximum of 100:
| Dimension | Max | 🟢 High (16–20) | 🟡 Medium (8–15) | 🔴 Low (0–7) | |-----------|-----|-----------------|-------------------|--------------| | Threat Block Rate | 20 | ≥95% of threats not in inbox (post-ZAP final state) | 80–94% remediated | <80% remediated (threats remain in inbox) | | Email Authentication | 20 | SPF+DMARC+DKIM all ≥95% | Any one 80–94% | Any one <80% | | ZAP Effectiveness | 20 | ≥95% ZAP success rate + 0 failed ZAPs | 80–94% success OR 1–2 failures | <80% success OR ≥3 failures | | Safe Links Protection | 20 | 0 phishing click-throughs AND active scanning | 1–2 phishing click-throughs | ≥3 phishing click-throughs OR no scanning | | Phishing Delivery Rate | 20 | 0 phishing emails delivered (post-ZAP) | 1–5 phishing delivered (post-ZAP) | >5 phishing still in mailboxes (post-ZAP) |
| Score | Rating | Action | |-------|--------|--------| | 85–100 | ✅ Strong | Excellent posture — maintain current configurations | | 65–84 | 🟡 Good | Minor gaps — review flagged dimensions | | 45–64 | 🟠 Needs Improvement | Multiple weaknesses — prioritize remediation | | 0–44 | 🔴 Critical | Significant exposure — immediate action required |
RunAdvancedHuntingQuery is available (EmailEvents tables are AH-native)Run in parallel — no dependencies between queries.
| Query | Purpose | |-------|---------| | Q1 | Inbound email summary with threat breakdown | | Q2 | Email volume trend by day | | Q3 | Delivery action and location breakdown | | Q4 | Detection methods breakdown |
Run in parallel — no dependencies between queries.
| Query | Purpose | |-------|---------| | Q5 | Email authentication pass rates (DMARC/DKIM/SPF/CompAuth) | | Q6 | ZAP and post-delivery remediation summary | | Q7 | Safe Links click activity summary | | Q8 | Phishing emails delivered (not blocked) |
Run in parallel — no dependencies between queries.
| Query | Purpose | |-------|---------| | Q9 | Top phishing sender domains | | Q10 | Most targeted recipients (aggregated) | | Q11 | Attachment type distribution | | Q12 | Post-ZAP threat state (latest delivery location) |
Run in parallel — no dependencies between queries.
| Query | Purpose | |-------|--------| | Q13 | MDO incident summary by severity and status | | Q14 | MDO incident type breakdown (top alert-driven incidents) |
⚠️ SecurityAlert.Status is IMMUTABLE — always "New" regardless of actual state. These queries use the canonical SecurityAlert→SecurityIncident join to get real Status and Classification from the SecurityIncident table. See copilot-instructions.md Known Table Pitfalls.
All queries below are verified against the EmailEvents family of tables. Use them exactly as written, substituting only the lookback period where noted. These queries use
Timestampfor Advanced Hunting. If falling back to Data Lake, replaceTimestampwithTimeGenerated.
EmailEvents
| where Timestamp > ago(7d)
| where EmailDirection == "Inbound"
| summarize
TotalInbound = count(),
Clean = countif(isempty(ThreatTypes)),
Phish = countif(ThreatTypes has "Phish"),
Malware = countif(ThreatTypes has "Malware"),
Spam = countif(ThreatTypes has "Spam"),
HighConfPhish = countif(ConfidenceLevel has "High" and ThreatTypes has "Phish"),
Blocked = countif(DeliveryAction == "Blocked"),
Delivered = countif(DeliveryAction == "Delivered"),
Junked = countif(DeliveryAction == "Junked"),
DistinctSenders = dcount(SenderFromAddress),
DistinctRecipients = dcount(RecipientEmailAddress)
| project TotalInbound, Clean, Phish, Malware, Spam, HighConfPhish,
Blocked, Delivered, Junked, DistinctSenders, DistinctRecipients
EmailEvents
| where Timestamp > ago(7d)
| summarize
Inbound = countif(EmailDirection == "Inbound"),
Outbound = countif(EmailDirection == "Outbound"),
IntraOrg = countif(EmailDirection == "Intra-org")
by Day = bin(Timestamp, 1d)
| order by Day asc
EmailEvents
| where Timestamp > ago(7d)
| where EmailDirection == "Inbound"
| summarize Count = count() by DeliveryAction, DeliveryLocation
| order by Count desc
EmailEvents
| where Timestamp > ago(7d)
| where isnotempty(DetectionMethods) and DetectionMethods != "{}"
| extend DetMethods = parse_json(DetectionMethods)
| extend FirstDetection = tostring(bag_keys(DetMethods)[0])
| extend FirstSubcategory = iif(
FirstDetection != "" and array_length(DetMethods[FirstDetection]) > 0,
strcat(FirstDetection, ": ", tostring(DetMethods[FirstDetection][0])),
FirstDetection)
| summarize Count = count() by FirstSubcategory
| order by Count desc
EmailEvents
| where Timestamp > ago(7d)
| where EmailDirection == "Inbound"
| extend AuthDetails = parse_json(AuthenticationDetails)
| extend
DMARC = tostring(AuthDetails.DMARC),
DKIM = tostring(AuthDetails.DKIM),
SPF = tostring(AuthDetails.SPF),
CompAuth = tostring(AuthDetails.CompAuth)
| summarize
TotalEmails = count(),
DMARCPass = countif(DMARC == "pass"),
DMARCFail = countif(DMARC == "fail"),
DKIMPass = countif(DKIM == "pass"),
DKIMFail = countif(DKIM == "fail"),
SPFPass = countif(SPF == "pass"),
SPFFail = countif(SPF == "fail"),
CompAuthPass = countif(CompAuth has "pass"),
CompAuthFail = countif(CompAuth == "fail")
EmailPostDeliveryEvents
| where Timestamp > ago(7d)
| summarize
TotalActions = count(),
PhishZAP = countif(ActionType == "Phish ZAP"),
MalwareZAP = countif(ActionType == "Malware ZAP"),
SpamZAP = countif(ActionType == "Spam ZAP"),
ThreatZAPTotal = countif(ActionType in ("Phish ZAP", "Malware ZAP", "Spam ZAP")),
ManualRemediation = countif(ActionType has "Admin"),
SuccessCount = countif(ActionResult == "Success"),
ErrorCount = countif(ActionResult == "Error")
| project TotalActions, PhishZAP, MalwareZAP, SpamZAP, ThreatZAPTotal, ManualRemediation, SuccessCount, ErrorCount
UrlClickEvents
| where Timestamp > ago(7d)
| summarize
TotalClicks = count(),
BlockedClicks = countif(ActionType == "ClickBlocked"),
AllowedClicks = countif(ActionType == "ClickAllowed"),
ClickedThrough = countif(IsClickedThrough == true),
PhishClicks = countif(ThreatTypes has "Phish"),
DistinctUrls = dcount(Url),
DistinctUsers = dcount(AccountUpn)
EmailEvents
| where Timestamp > ago(7d)
| where ThreatTypes has "Phish"
| where DeliveryAction == "Delivered" or LatestDeliveryAction == "Delivered"
| summarize
DeliveredPhish = count(),
DistinctRecipients = dcount(RecipientEmailAddress),
DistinctSenders = dcount(SenderFromAddress),
Subjects = make_set(Subject, 5)
EmailEvents
| where Timestamp > ago(7d)
| where ThreatTypes has "Phish"
| summarize
Count = count(),
DistinctRecipients = dcount(RecipientEmailAddress),
DeliveredCount = countif(DeliveryAction == "Delivered" or LatestDeliveryAction == "Delivered")
by SenderFromDomain
| top 10 by Count
EmailEvents
| where Timestamp > ago(7d)
| where isnotempty(ThreatTypes) and EmailDirection == "Inbound"
| extend RecipientDomain = tostring(split(RecipientEmailAddress, "@")[1])
| summarize
ThreatCount = count(),
PhishCount = countif(ThreatTypes has "Phish"),
SpamCount = countif(ThreatTypes has "Spam"),
MalwareCount = countif(ThreatTypes has "Malware"),
DistinctRecipients = dcount(RecipientEmailAddress)
by RecipientDomain
| order by ThreatCount desc
EmailAttachmentInfo
| where Timestamp > ago(7d)
| summarize
Count = count(),
DistinctFiles = dcount(FileName),
ThreatCount = countif(isnotempty(ThreatTypes))
by FileType
| order by Count desc
| take 15
EmailEvents
| where Timestamp > ago(7d)
| where EmailDirection == "Inbound"
| where isnotempty(ThreatTypes)
| summarize Count = count() by LatestDeliveryAction, LatestDeliveryLocation, ThreatTypes
| order by Count desc
Uses the canonical SecurityAlert→SecurityIncident join. Filters to
ProductName == "Office 365 Advanced Threat Protection"and excludes Communication Compliance alerts (CC_prefix).
let MDOAlerts = SecurityAlert
| where TimeGenerated > ago(7d)
| where ProductName == "Office 365 Advanced Threat Protection"
| where AlertName !startswith "CC_"
| summarize arg_max(TimeGenerated, *) by SystemAlertId
| project SystemAlertId;
SecurityIncident
| where CreatedTime > ago(7d)
| summarize arg_max(TimeGenerated, *) by IncidentNumber
| mv-expand AlertId = AlertIds
| extend AlertId = tostring(AlertId)
| join kind=inner MDOAlerts on $left.AlertId == $right.SystemAlertId
| summarize IncidentCount = dcount(IncidentNumber) by Severity, Status, Classification
| order by Severity asc, IncidentCount desc
Groups incidents by title and alert composition to show the most common MDO-generated incident types.
let MDOAlerts = SecurityAlert
| where TimeGenerated > ago(7d)
| where ProductName == "Office 365 Advanced Threat Protection"
| where AlertName !startswith "CC_"
| summarize arg_max(TimeGenerated, *) by SystemAlertId
| project SystemAlertId, AlertName, AlertSeverity, ProductName;
SecurityIncident
| where CreatedTime > ago(7d)
| summarize arg_max(TimeGenerated, *) by IncidentNumber
| mv-expand AlertId = AlertIds
| extend AlertId = tostring(AlertId)
| join kind=inner MDOAlerts on $left.AlertId == $right.SystemAlertId
| summarize
IncidentCount = dcount(IncidentNumber),
AlertCount = count(),
OpenCount = dcountif(IncidentNumber, Status == "New" or Status == "Active"),
ClosedCount = dcountif(IncidentNumber, Status == "Closed"),
TruePositives = dcountif(IncidentNumber, Classification == "TruePositive")
by AlertName, Severity
| order by IncidentCount desc
| take 10
Render the full analysis directly in the chat response. Best for quick review and C-level briefings.
Save a comprehensive report to disk at:
reports/email-threat-posture/Email_Threat_Protection_Report_YYYYMMDD_HHMMSS.md
Generate the markdown file AND provide an inline summary in chat.
Always ask the user which mode before generating output.
Render the following sections in order. Omit sections only if explicitly noted as conditional.
🔴 URL Rule: All hyperlinks in the report MUST be copied verbatim from the URL Registry above. Do NOT generate, recall from memory, or paraphrase any URL. If a needed URL is not in the registry, use plain text (no hyperlink).
# 📧 Email Threat Protection Report
**Generated:** YYYY-MM-DD HH:MM UTC
**Data Source:** Microsoft Defender for Office 365 (Advanced Hunting)
**Analysis Period:** <StartDate> → <EndDate> (<N> days)
**Protected Mailboxes:** <DistinctRecipients>
**Total Inbound Emails:** <N>
**Email Protection Score:** <Score>/100 — <RATING>
---
## Executive Summary
<2-3 sentences: total inbound volume, threat detection rate, key findings, overall posture rating>
**Email Protection Score:** 🟢/🟡/🟠/🔴 <RATING> (<Score>/100)
---
## Key Metrics
| Metric | Value |
|--------|-------|
| Total Inbound Emails | <N> |
| Clean Email Rate | <N>% (<clean> of <total>) |
| Threats Detected | <N> (Phish: <N>, Spam: <N>, Malware: <N>) |
| Threats Blocked Pre-Delivery | <N> |
| Phishing Delivered (Now Remediated) | <N> |
| Threat ZAP Actions | <N> (Phish: <N>, Malware: <N>, Spam: <N>) |
| Total Post-Delivery Actions | <N> (includes system events) |
| ZAP Success Rate | <N>% (Failed: <N>) |
| Threats Still in Mailboxes (Post-ZAP) | <N> (Phish: <N>, Spam: <N>) |
| Safe Links Clicks Scanned | <N> |
| Phishing Click-Throughs | <N> |
| Distinct Senders | <N> |
| Protected Mailboxes | <N> |
---
## 📬 Mail Flow Overview
### Daily Volume Trend
<Table or sparkline showing inbound/outbound/intra-org by day>
| Day | Inbound | Outbound | Intra-org |
|-----|---------|----------|-----------|
| <date> | <N> | <N> | <N> |
**Observations:** <Note any spikes, trends, or anomalies>
---
## 🛡️ Threat Composition
### Threat Categories
| Category | Count | % of Threats |
|----------|-------|-------------|
| Phishing | <N> | <N>% |
| Spam | <N> | <N>% |
| Malware | <N> | <N>% |
| High-Confidence Phishing | <N> | — |
### Detection Methods
| Method | Count |
|--------|-------|
| <method> | <N> |
### Top Phishing Sender Domains
| Domain | Phish Count | Delivered | Recipients Hit |
|--------|-------------|-----------|----------------|
| <domain> | <N> | <N> | <N> |
<If Q9 returns 0 phishing domains:>
✅ No phishing sender domains detected.
---
## 📦 Delivery Disposition
### Initial Delivery Action
| Action | Location | Count |
|--------|----------|-------|
| Delivered | Inbox/folder | <N> |
| Blocked | Dropped | <N> |
| Blocked | Quarantine | <N> |
| Junked | Junk folder | <N> |
### Post-ZAP Threat State
<Shows where threats currently reside after ZAP remediation>
| Latest Action | Location | Threat Type | Count |
|---------------|----------|-------------|-------|
| <action> | <location> | <type> | <N> |
**Summary of current threat locations (post-ZAP):**
| Current Location | Threat Count | % of Threats |
|-----------------|-------------|-------------|
| 🟢 Quarantine | <N> | <N>% |
| 🟢 Junk folder | <N> | <N>% |
| 🟢 Blocked/Dropped/Failed | <N> | <N>% |
| 🟢 Deleted items | <N> | <N>% |
| 🔴 **Still in Inbox** | **<N>** | **<N>%** |
| **Total** | **<N>** | **100%** |
> Show the phishing vs spam breakdown for "Still in Inbox": e.g., "<N> phishing (<N> total threats including spam)"
---
## 🔐 Email Authentication
| Protocol | Pass Rate | Pass Count | Fail Count | Other/None |
|----------|-----------|------------|------------|------------|
| SPF | <N>% | <N> | <N> | <N> |
| DMARC | <N>% | <N> | <N> | <N> |
| DKIM | <N>% | <N> | <N> | <N> |
| CompAuth | <N>% | <N> | <N> | <N> |
> **Note:** "Other/None" = emails with no result for that protocol (e.g., no DKIM signature). A low DKIM pass rate with 0 failures means unsigned senders, not spoofing. Compare against DMARC and CompAuth for the complete authentication picture.
**Assessment:**
- <emoji> <finding for each protocol>
---
## 🧹 Post-Delivery Remediation (ZAP)
| Metric | Value |
|--------|-------|
| Threat ZAP Actions | <N> (Phish: <N>, Malware: <N>, Spam: <N>) |
| Total Post-Delivery Actions | <N> (includes system events, admin actions) |
| ZAP Success Rate | <N>% (<success> of <total>) |
| Failed Remediations | <N> |
> **Reporting guidance:** The Key Metrics "Threat ZAP Actions" row should show **only** the Phish + Malware + Spam ZAP count — NOT the TotalActions, which includes system-initiated post-delivery events (message trace updates, delivery location changes). TotalActions is shown separately with a clarifying note.
<If ErrorCount > 0:>
⚠️ **<N> ZAP remediation(s) failed** — manual follow-up recommended. Threats may remain in user mailboxes.
<If ErrorCount == 0:>
✅ All post-delivery remediations completed successfully.
---
## 🔗 Safe Links Protection
| Metric | Value |
|--------|-------|
| Total Clicks Scanned | <N> |
| Clicks Blocked | <N> |
| Clicks Allowed | <N> |
| Phishing Clicks | <N> |
| Click-Through Overrides | <N> |
| Distinct URLs Scanned | <N> |
| Users Protected | <N> |
<If PhishClicks > 0:>
🔴 **<N> phishing URL click(s) detected** — investigate affected users for credential compromise.
<If PhishClicks == 0:>
✅ No phishing URL click-throughs detected.
---
## 📎 Attachment Analysis
### Top Attachment Types
| File Type | Count | Distinct Files | Threats Detected |
|-----------|-------|----------------|------------------|
| <type> | <N> | <N> | <N> |
<If any ThreatCount > 0:>
⚠️ **Malicious attachments detected in <N> file type(s)** — verify delivery status and endpoint execution.
<If all ThreatCount == 0:>
✅ No malicious attachments detected in email flow.
---
## 🎯 Targeted Recipients
| Recipient Domain | Threat Count | Phish | Spam | Malware | Recipients |
|-----------------|-------------|-------|------|---------|------------|
| <domain> | <N> | <N> | <N> | <N> | <N> |
---
## Email Protection Score Card
```
┌──────────────────────────────────────────────────────┐
│ EMAIL PROTECTION SCORE: <NN>/100 │
│ Rating: <EMOJI> <RATING> │
├──────────────────────────────────────────────────────┤
│ Threat Block Rate [<bar>] <N>/20 (<detail>) │
│ Email Authentication [<bar>] <N>/20 (<detail>) │
│ ZAP Effectiveness [<bar>] <N>/20 (<detail>) │
│ Safe Links Protection[<bar>] <N>/20 (<detail>) │
│ Phishing Delivery [<bar>] <N>/20 (<detail>) │
└──────────────────────────────────────────────────────┘
```
---
## 🚨 MDO Security Incidents
### Incident Summary (Last <N> Days)
| Severity | Open | Closed | True Positive | Total |
|----------|------|--------|---------------|-------|
| 🔴 High | <N> | <N> | <N> | <N> |
| 🟠 Medium | <N> | <N> | <N> | <N> |
| 🟡 Low | <N> | <N> | <N> | <N> |
| 🔵 Informational | <N> | <N> | <N> | <N> |
| **Total** | **<N>** | **<N>** | **<N>** | **<N>** |
### Top MDO Incident Types
| Alert Name | Severity | Incidents | Open | Closed | True Positives |
|------------|----------|-----------|------|--------|----------------|
| <name> | <sev> | <N> | <N> | <N> | <N> |
<If Q13 returns 0 incidents:>
✅ No MDO-generated security incidents in the analysis period.
---
## Security Assessment
| Factor | Finding |
|--------|---------|
| <emoji> **<Factor>** | <Evidence-based finding> |
---
## Recommendations
1. <emoji> **<Priority action>** — <evidence and rationale>
2. ...
---
## Appendix: Query Execution Summary
| Query | Description | Records | Time |
|-------|-------------|---------|------|
| Q1 | Inbound Email Summary | <N> | <time> |
| Q2 | Daily Volume Trend | <N> | <time> |
| ... | ... | ... | ... |
| Q13 | MDO Incident Summary | <N> | <time> |
| Q14 | MDO Incident Types | <N> | <time> |
When outputting to markdown file, use the same structure as the Inline Report Template above, saved to:
reports/email-threat-posture/Email_Threat_Protection_Report_YYYYMMDD_HHMMSS.md
Include the following additional sections in the file report that are omitted from inline:
Follow this exact section order in markdown file reports:
| Order | Section | Source | |-------|---------|--------| | 1 | Header (with Total Inbound + Score) | Template header | | 2 | Executive Summary | Template | | 3 | Key Metrics | Template | | 4 | Mail Flow Overview (daily trend) | Q2 | | 5 | Threat Composition (categories + detection methods + top phish senders) | Q1, Q4, Q9 | | 6 | Delivery Disposition (initial + post-ZAP threat state) | Q3, Q12 | | 7 | Email Authentication (with auth failures by domain) | Q5, QM2 | | 8 | Post-Delivery Remediation (ZAP) | Q6 | | 9 | Safe Links Protection | Q7 | | 10 | Attachment Analysis | Q11 | | 11 | Targeted Recipients | Q10 | | 12 | — Deep-dive sections start here — | | | 13 | Overridden Threats | QM3 | | 14 | First-Contact Phishing | QM4 | | 15 | MDO Security Incidents | Q13, Q14 | | 16 | Top Sender Domains by Volume | QM1 | | 17 | — Score and assessment — | | | 18 | Email Protection Score Card | Computed | | 19 | Security Assessment | Synthesized | | 20 | Recommendations | Synthesized | | 21 | Appendix: Query Execution Summary | All queries | | 22 | References | URL Registry |
Key rule: Score Card → Assessment → Recommendations always come AFTER all data sections (including deep dives). This ensures the reader sees all evidence before the overall assessment.
These queries provide enrichment data for the markdown file report only. Skip for inline mode.
EmailEvents
| where Timestamp > ago(7d)
| where EmailDirection == "Inbound"
| summarize
EmailCount = count(),
PhishCount = countif(ThreatTypes has "Phish"),
SpamCount = countif(ThreatTypes has "Spam"),
DistinctSenders = dcount(SenderFromAddress)
by SenderFromDomain
| order by EmailCount desc
| take 10
EmailEvents
| where Timestamp > ago(7d)
| where EmailDirection == "Inbound"
| extend AuthDetails = parse_json(AuthenticationDetails)
| extend
DMARC = tostring(AuthDetails.DMARC),
DKIM = tostring(AuthDetails.DKIM),
SPF = tostring(AuthDetails.SPF)
| summarize
TotalEmails = count(),
DMARCFail = countif(DMARC == "fail"),
DKIMFail = countif(DKIM == "fail"),
SPFFail = countif(SPF == "fail")
by SenderFromDomain
| where DMARCFail > 0 or DKIMFail > 0 or SPFFail > 0
| order by TotalEmails desc
| take 15
EmailEvents
| where Timestamp > ago(7d)
| where OrgLevelAction == "Allow" and isnotempty(ThreatTypes)
| summarize Count = count() by ThreatTypes, OrgLevelPolicy, DetectionMethods
| order by Count desc
EmailEvents
| where Timestamp > ago(7d)
| where EmailDirection == "Inbound"
| where IsFirstContact == true
| where ThreatTypes has "Phish" or UrlCount > 3
| summarize
FirstContactCount = count(),
PhishCount = countif(ThreatTypes has "Phish"),
HighUrlCount = countif(UrlCount > 3),
DistinctSenders = dcount(SenderFromAddress)
# Email Threat Protection Report
**Generated:** YYYY-MM-DD HH:MM UTC
**Data Source:** Microsoft Defender for Office 365 (Advanced Hunting)
**Analysis Period:** <StartDate> → <EndDate> (<N> days)
**Protected Mailboxes:** <DistinctRecipients>
**Total Inbound Emails:** <N>
**Email Protection Score:** <Score>/100 — <RATING>
---
Problem: DetectionMethods looks like it should be dynamic but is a string column containing JSON. Direct property access fails.
Solution: Always parse_json(DetectionMethods) before accessing sub-keys:
| extend DetMethods = parse_json(DetectionMethods)
| extend FirstDetection = tostring(bag_keys(DetMethods)[0])
Problem: Same as DetectionMethods — AuthenticationDetails is a string column, not dynamic.
Solution: Always parse_json(AuthenticationDetails):
| extend AuthDetails = parse_json(AuthenticationDetails)
| extend DMARC = tostring(AuthDetails.DMARC)
Problem: ThreatTypes can contain multiple values pipe-delimited (e.g., "Phish|Spam"). Using == will miss multi-category threats.
Solution: Always use has operator:
| where ThreatTypes has "Phish" // ✅ Correct
| where ThreatTypes == "Phish" // ❌ Misses "Phish|Spam"
Problem: Advanced Hunting uses Timestamp for XDR-native tables. Sentinel Data Lake uses TimeGenerated.
Solution: Default queries use Timestamp (AH). If falling back to Data Lake, replace Timestamp with TimeGenerated throughout.
Problem: IsFirstContact can be null for outbound or intra-org emails. Filtering on it without scoping to inbound emails may miss records.
Solution: Always filter EmailDirection == "Inbound" before using IsFirstContact.
Problem: DeliveryAction is the initial delivery disposition. LatestDeliveryAction reflects the current state after ZAP or manual remediation. Reporting only DeliveryAction overstates the number of threats in mailboxes.
Solution: When assessing current threat exposure, use LatestDeliveryAction and LatestDeliveryLocation. When assessing initial filter effectiveness, use DeliveryAction.
Problem: DKIM pass rate can appear low because many legitimate emails (especially bulk/marketing) don't sign with DKIM at all. An email with no DKIM signature isn't a DKIM "fail" — it simply has no result. The DKIM field from AuthenticationDetails may be empty or "none" rather than "fail".
Solution: When computing DKIM pass rate, note the denominator: emails with a DKIM result vs total emails. A lower DKIM rate is expected and doesn't necessarily indicate spoofing. Compare against DMARC and CompAuth for a better authentication picture.
Problem: ZAP errors can occur for legitimate reasons: shared mailboxes, retention policies preventing purge, user-moved emails. A ZAP error doesn't always mean a threat is still active.
Solution: When reporting ZAP failures, note that manual investigation may confirm the threat was already handled. Don't over-alarm on ZAP errors without context.
Problem: EmailPostDeliveryEvents includes all post-delivery events — not just ZAP threat remediations. The TotalActions count from Q6 includes system-initiated events (message trace updates, delivery location changes, admin investigation submissions). Reporting TotalActions as "ZAP Remediations" in Key Metrics massively overstates the threat remediation picture (e.g., 7,790 total when only 674 are actual threat ZAPs).
Solution: Always use ThreatZAPTotal (PhishZAP + MalwareZAP + SpamZAP) for headline ZAP metrics. Show TotalActions separately with a clarifying note: "includes system events". In Key Metrics, use "Threat ZAP Actions: 674" not "ZAP Remediations: 7,790".
Problem: The scoring dimension "Threat Block Rate" can be interpreted two ways: (a) pre-delivery block rate (threats blocked before reaching inbox), or (b) final disposition rate (threats not in inbox after ZAP). These give different numbers — e.g., 72.5% pre-delivery vs 81.2% post-ZAP.
Solution: The dimension measures final threat disposition (post-ZAP) — the percentage of detected threats that are NOT currently in user inboxes. This is the operationally relevant metric because it reflects actual user exposure. The dimension description explicitly says "not in inbox (post-ZAP final state)".
Before delivering the report, verify:
Timestamp (AH) or TimeGenerated (Data Lake) consistently📊 Optional post-report step. After an Email Threat Protection report is generated, the user can request a visual SVG dashboard.
Trigger phrases: "generate SVG dashboard", "create a visual dashboard", "visualize this report", "SVG from the report"
#file:reports/email-threat-posture/Email_Threat_Protection_Report_<date>.mdStep 1: Read svg-widgets.yaml (this skill's widget manifest)
Step 2: Read .github/skills/svg-dashboard/SKILL.md (rendering rules — Manifest Mode)
Step 3: Read the completed report file (data source)
Step 4: Render SVG → save to reports/email-threat-posture/{report_name}_dashboard.svg
The YAML manifest is the single source of truth for layout, widgets, field mappings, colors, and data source documentation. All customization happens there.
development
Use this skill when asked to investigate a computer, device, endpoint, or machine for security issues, suspicious activity, malware, or compliance review. Triggers on keywords like "investigate computer", "investigate device", "investigate endpoint", "check machine", "device security", "endpoint investigation", or when a device name/hostname is mentioned with investigation context. This skill provides comprehensive device security analysis including Defender alerts, sign-in patterns, logged-on users, vulnerabilities, software inventory, compliance status, network activity, and automated investigation tracking for Entra Joined, Hybrid Joined, and Entra Registered devices.
development
Recommended starting point for new users and daily SOC operations. Quick 15-minute security posture scan across 7 domains: active incidents, identity (human + NonHuman), endpoint, email threats, admin & cloud ops, and exposure. 12 queries executed in parallel batches, producing a prioritized Threat Pulse Dashboard with color-coded verdicts (🔴 Escalate / 🟠 Investigate / 🟡 Monitor / ✅ Clear) and drill-down recommendations pointing to specialized skills. Trigger on getting-started questions like "what can you do", "where do I start", "help me investigate". Supports inline chat and markdown file output
development
Use this skill when asked to investigate a user account for security issues, suspicious activity, or compliance review. Triggers on keywords like "investigate user", "security investigation", "user investigation", "check user activity", "analyze sign-ins", or when a UPN/email is mentioned with investigation context. This skill provides comprehensive Entra ID user security analysis including sign-in anomalies, MFA status, device compliance, audit logs, security incidents, Identity Protection risk, and automated reports (HTML, markdown file, or inline chat).
development
Use this skill when asked to generate SVG data visualization dashboards from investigation data or skill reports. Triggers on keywords like "generate SVG dashboard", "create a visual dashboard", "visualize this report", "SVG from the report", "visualize results", "create SVG chart", "SVG from this data". Supports two modes: manifest-driven structured dashboards (from skill reports with svg-widgets.yaml) and freeform adaptive visualizations from ad-hoc investigation data. Component library includes KPI cards, score cards, bar charts, line charts, donut charts, waterfall charts, tables, recommendation cards, assessment banners. SharePoint Dark Theme default palette.