Forum Discussion
Campaign-Centric Hunting with Microsoft Defender XDR and Microsoft Sentinel
Phishing investigations usually start with one suspicious email.
A user reports a message.
An alert is generated.
An analyst opens the email details, checks the sender, reviews the URL, and tries to understand whether the message is malicious.
That is a normal starting point. However, in a real SOC investigation, one email is rarely the full story.
Attackers usually operate in campaigns. They reuse sender infrastructure, similar subjects, URLs, payloads, templates, and delivery techniques. A single email may be only one part of a wider phishing or malware campaign targeting multiple users.
This is why campaign-centric hunting is important.
I wrote this article from the perspective of a SOC analyst who often needs to move quickly from a single suspicious email to the full campaign impact. The goal is simple: use Microsoft Defender XDR and Microsoft Sentinel together to understand who was targeted, what was delivered, who clicked, and what should be prioritized first.
Why Campaign-Centric Hunting
When investigating a phishing or malware email, analysts usually need to answer practical questions:
- How many users received messages from the same campaign?
- Were the messages blocked, junked, delivered, or remediated?
- Did any user click the URL?
- Did anyone click through a Safe Links warning?
- Were any priority or high-risk users affected?
- Was the email removed after delivery?
- Are there related Defender XDR or Sentinel incidents?
If we only investigate one message, we may miss the bigger picture.
Campaign-centric hunting helps the SOC move from this question:
Is this email malicious?
To this question:
What is the full impact of this campaign?
That shift is important because the response priority should be based on campaign impact, not only on a single alert.
What Campaign Views Provides
Campaign Views in Microsoft Defender for Office 365 help analysts investigate coordinated email attacks such as phishing and malware campaigns.
From Campaign Views, analysts can review campaign-level information such as:
- Campaign name
- Campaign type
- Campaign subtype
- Targeted users
- Inboxed messages
- Clicked users
- Visited links
- Sender domains
- Sender IPs
- Payload URLs
- Delivery actions
- Campaign timeline
- Campaign flow
This is useful during triage because it quickly shows whether an email is part of a wider attack.
For example, one reported phishing message may look small at first. But if Campaign Views shows that the same campaign targeted 50 users, delivered messages to 15 inboxes, and had 2 users click the URL, the investigation becomes much more urgent.
Where CampaignInfo Fits
The CampaignInfo table gives analysts a KQL-based way to query campaign-related data.
Some useful fields are:
Field | Purpose |
CampaignId | Unique identifier for the campaign |
CampaignName | Name of the campaign |
CampaignType | Campaign category, such as Phish or Malware |
CampaignSubtype | Additional context, such as brand being phished or malware family |
NetworkMessageId | Unique identifier for the email message |
RecipientEmailAddress | Recipient affected by the campaign |
Timestamp | Time when the event was recorded |
For correlation, the most important field is usually:
NetworkMessageId
This field can help connect campaign data with other Defender XDR email tables, including:
- EmailEvents
- UrlClickEvents
- EmailPostDeliveryEvents
- EmailAttachmentInfo
- EmailUrlInfo
This makes CampaignInfo a useful pivot table for campaign-level hunting.
Important note: CampaignInfo is currently documented as Preview. Before using these queries in production analytics rules, validate the table availability, schema, and results in your own tenant.
Practical Scenario
An analyst receives a phishing alert in Microsoft Defender XDR. The alert is related to a user who received a suspicious email with a credential-harvesting URL.
The analyst opens Campaign Views and sees that the message belongs to a wider phishing campaign.
At that point, the investigation should not stop with the original user.
The analyst should now ask:
- Who else received this campaign?
- How many messages were delivered?
- Which users clicked?
- Did any users click through the Safe Links warning?
- Were the messages removed after delivery?
- Are there related incidents in Microsoft Sentinel?
The investigation flow could look like this:
- Start from Campaign Views in Microsoft Defender XDR.
- Identify the campaign details.
- Use CampaignInfo to list affected users and messages.
- Join with EmailEvents to validate delivery status.
- Join with UrlClickEvents to identify user interaction.
- Join with EmailPostDeliveryEvents to confirm remediation.
- Review related Microsoft XDR incidents in Microsoft Sentinel.
- Prioritize response based on campaign impact.
Query 1: List Recent Campaigns
The first query gives a simple overview of recent campaigns.
CampaignInfo
| where Timestamp > ago(14d)
| summarize
FirstSeen = min(Timestamp),
LastSeen = max(Timestamp),
AffectedUsers = dcount(RecipientEmailAddress),
Messages = dcount(NetworkMessageId)
by CampaignId, CampaignName, CampaignType, CampaignSubtype
| order by LastSeen desc
This helps analysts quickly identify campaigns that affected the organization during the selected period.
Useful questions to ask from this output:
- Which campaigns are most recent?
- Which campaigns affected the most users?
- Are the campaigns phishing, malware, or spam?
- Is there a specific brand or malware family in the subtype?
- Are similar campaigns appearing repeatedly?
Query 2: Understand Delivery Impact
After identifying campaigns, the next step is to understand delivery impact.
A campaign that was fully blocked is different from a campaign that reached user inboxes.
let Campaigns =
CampaignInfo
| where Timestamp > ago(14d)
| project
CampaignId,
CampaignName,
CampaignType,
CampaignSubtype,
NetworkMessageId,
RecipientEmailAddress;
Campaigns
| join kind=leftouter (
EmailEvents
| where Timestamp > ago(14d)
| project
NetworkMessageId,
RecipientEmailAddress,
Subject,
SenderFromAddress,
SenderFromDomain,
SenderIPv4,
DeliveryAction,
DeliveryLocation,
ThreatTypes,
DetectionMethods,
Timestamp
) on NetworkMessageId, RecipientEmailAddress
| summarize
Messages = dcount(NetworkMessageId),
AffectedUsers = dcount(RecipientEmailAddress),
Subjects = make_set(Subject, 5),
SenderDomains = make_set(SenderFromDomain, 10),
SenderIPs = make_set(SenderIPv4, 10)
by CampaignId, CampaignName, CampaignType, CampaignSubtype, DeliveryAction, DeliveryLocation
| order by AffectedUsers desc, Messages desc
This query helps separate campaigns that were blocked from campaigns that actually reached users.
From a SOC perspective, delivered messages deserve closer attention, especially if they reached the inbox.
Query 3: Identify Users Who Clicked Campaign URLs
Delivery is important, but clicks usually increase the priority of the incident.
This query joins campaign data with UrlClickEvents.
let Campaigns =
CampaignInfo
| where Timestamp > ago(14d)
| project
CampaignId,
CampaignName,
CampaignType,
CampaignSubtype,
NetworkMessageId,
RecipientEmailAddress;
Campaigns
| join kind=inner (
UrlClickEvents
| where Timestamp > ago(14d)
| project
NetworkMessageId,
AccountUpn,
Url,
ActionType,
IsClickedThrough,
ThreatTypes,
DetectionMethods,
IPAddress,
Workload,
ClickTime = Timestamp
) on NetworkMessageId
| summarize
FirstClick = min(ClickTime),
LastClick = max(ClickTime),
ClickEvents = count(),
ClickedUsers = dcount(AccountUpn),
ClickThroughUsers = dcountif(AccountUpn, IsClickedThrough == true),
ClickedUrls = make_set(Url, 10),
SourceIPs = make_set(IPAddress, 10)
by CampaignId, CampaignName, CampaignType, CampaignSubtype
| order by ClickThroughUsers desc, ClickedUsers desc, LastClick desc
This query helps identify campaigns where users interacted with the payload.
If a user clicked a phishing URL, the next step should usually include identity-focused investigation, such as reviewing sign-in activity, MFA status, session activity, and possible risky sign-ins.
Query 4: Focus on Click-Through Events
Safe Links may block access to a malicious site. In some cases, however, a user may continue through a warning page.
Those cases should be reviewed carefully.
let Campaigns =
CampaignInfo
| where Timestamp > ago(30d)
| project
CampaignId,
CampaignName,
CampaignType,
CampaignSubtype,
NetworkMessageId,
RecipientEmailAddress;
Campaigns
| join kind=inner (
UrlClickEvents
| where Timestamp > ago(30d)
| where IsClickedThrough == true
| project
NetworkMessageId,
AccountUpn,
Url,
ActionType,
ThreatTypes,
IPAddress,
ClickTime = Timestamp
) on NetworkMessageId
| project
ClickTime,
CampaignId,
CampaignName,
CampaignType,
CampaignSubtype,
AccountUpn,
RecipientEmailAddress,
Url,
ActionType,
ThreatTypes,
IPAddress
| order by ClickTime desc
This is one of the most useful views during incident response.
A click-through event does not automatically mean compromise, but it is a strong reason to investigate the user account further.
Query 5: Confirm Post-Delivery Remediation
A malicious message may be delivered first and removed later by ZAP, AIR, or manual remediation.
This query joins CampaignInfo with EmailPostDeliveryEvents.
let Campaigns =
CampaignInfo
| where Timestamp > ago(30d)
| project
CampaignId,
CampaignName,
CampaignType,
CampaignSubtype,
NetworkMessageId,
RecipientEmailAddress;
Campaigns
| join kind=leftouter (
EmailPostDeliveryEvents
| where Timestamp > ago(30d)
| project
NetworkMessageId,
RecipientEmailAddress,
RemediationTime = Timestamp,
Action,
ActionType,
ActionTrigger,
ActionResult,
DeliveryLocation,
SourceLocation
) on NetworkMessageId, RecipientEmailAddress
| summarize
RemediatedMessages = dcountif(NetworkMessageId, isnotempty(ActionType)),
RemediationTypes = make_set(ActionType, 10),
RemediationResults = make_set(ActionResult, 10),
LastRemediation = max(RemediationTime)
by CampaignId, CampaignName, CampaignType, CampaignSubtype
| order by LastRemediation desc
This helps answer a very important question:
Were the delivered malicious messages actually removed?
This is useful for both SOC triage and reporting because it shows not only detection, but also response.
Query 6: Campaign Blast Radius Summary
The following query combines campaign, delivery, click, and remediation data into one campaign-level view.
let TimeRange = 30d;
let Campaigns =
CampaignInfo
| where Timestamp > ago(TimeRange)
| project
CampaignId,
CampaignName,
CampaignType,
CampaignSubtype,
NetworkMessageId,
RecipientEmailAddress;
let Delivery =
EmailEvents
| where Timestamp > ago(TimeRange)
| summarize
DeliveryActions = make_set(DeliveryAction, 10),
DeliveryLocations = make_set(DeliveryLocation, 10),
DeliveredMessages = dcountif(NetworkMessageId, DeliveryAction =~ "Delivered"),
JunkedMessages = dcountif(NetworkMessageId, DeliveryAction =~ "Junked"),
BlockedMessages = dcountif(NetworkMessageId, DeliveryAction =~ "Blocked"),
Subjects = make_set(Subject, 5),
SenderDomains = make_set(SenderFromDomain, 10)
by NetworkMessageId, RecipientEmailAddress;
let Clicks =
UrlClickEvents
| where Timestamp > ago(TimeRange)
| summarize
ClickEvents = count(),
ClickThroughEvents = countif(IsClickedThrough == true),
FirstClick = min(Timestamp),
LastClick = max(Timestamp),
ClickedUrls = make_set(Url, 10)
by NetworkMessageId;
let Remediation =
EmailPostDeliveryEvents
| where Timestamp > ago(TimeRange)
| summarize
RemediationActions = make_set(ActionType, 10),
LastRemediation = max(Timestamp)
by NetworkMessageId, RecipientEmailAddress;
Campaigns
| join kind=leftouter Delivery on NetworkMessageId, RecipientEmailAddress
| join kind=leftouter Clicks on NetworkMessageId
| join kind=leftouter Remediation on NetworkMessageId, RecipientEmailAddress
| summarize
AffectedUsers = dcount(RecipientEmailAddress),
Messages = dcount(NetworkMessageId),
DeliveredMessages = sum(DeliveredMessages),
JunkedMessages = sum(JunkedMessages),
BlockedMessages = sum(BlockedMessages),
TotalClickEvents = sum(ClickEvents),
ClickThroughEvents = sum(ClickThroughEvents),
Subjects = make_set(Subjects, 10),
SenderDomains = make_set(SenderDomains, 10),
ClickedUrls = make_set(ClickedUrls, 10),
RemediationActions = make_set(RemediationActions, 10),
LastClick = max(LastClick),
LastRemediation = max(LastRemediation)
by CampaignId, CampaignName, CampaignType, CampaignSubtype
| extend SuggestedPriority =
case(
ClickThroughEvents > 0, "High",
TotalClickEvents > 0, "Medium",
DeliveredMessages > 0, "Medium",
"Low"
)
| order by SuggestedPriority asc, AffectedUsers desc, Messages desc
This type of query can be useful during hunting sessions, incident review, and campaign reporting.
The goal is not only to collect more data. The goal is to help the analyst decide what needs attention first.
Correlating Campaign Activity with Microsoft Sentinel
When Microsoft Defender XDR is connected to Microsoft Sentinel, incidents and alerts can be synchronized into the Sentinel incident queue.
This allows the SOC to correlate campaign-related email activity with other security signals, such as:
- Suspicious sign-ins
- Identity alerts
- Endpoint alerts
- Cloud app activity
- OAuth consent activity
- Data exfiltration attempts
- Related Microsoft XDR incidents
For example, if a user clicked a phishing URL, the SOC can then review whether the same user had suspicious sign-in activity shortly after the click.
The following query is a simple starting point for reviewing Microsoft XDR incidents in Microsoft Sentinel.
SecurityIncident
| where TimeGenerated > ago(30d)
| where ProviderName == "Microsoft XDR"
| where Title has_any ("phish", "phishing", "email", "malware", "campaign")
| summarize
Incidents = count(),
HighSeverity = countif(Severity == "High"),
MediumSeverity = countif(Severity == "Medium"),
Closed = countif(Status == "Closed"),
Active = countif(Status == "Active")
by bin(TimeGenerated, 1d)
| order by TimeGenerated desc
This query does not replace campaign hunting. It simply helps analysts understand how email-related activity is represented in the Sentinel incident queue.
Suggested SOC Workflow
A practical campaign-centric workflow could look like this:
Step 1: Start from Campaign Views
Review campaigns with delivered messages, clicked users, visited links, or high user impact.
Step 2: Pivot to KQL
Use CampaignInfo to list campaign-related messages and affected recipients.
Step 3: Validate Delivery
Join with EmailEvents to confirm whether messages were blocked, junked, delivered, or replaced.
Step 4: Review User Interaction
Join with UrlClickEvents to identify users who clicked URLs or clicked through Safe Links warnings.
Step 5: Confirm Remediation
Join with EmailPostDeliveryEvents to confirm whether delivered messages were removed after delivery.
Step 6: Correlate in Sentinel
Review related Microsoft XDR incidents and correlate with identity, endpoint, and cloud activity.
Step 7: Decide Response
Depending on the impact, the SOC may decide to:
- Escalate the incident
- Notify affected users
- Review user sign-ins
- Revoke user sessions
- Reset passwords
- Block sender domains or URLs
- Submit false negatives
- Create a watchlist for related indicators
- Tune analytics rules or response processes
Suggested Priority Logic
Not every campaign needs the same level of response.
A simple triage model could be:
Condition | Suggested priority |
Campaign blocked before delivery | Low |
Campaign delivered to junk | Low to Medium |
Campaign delivered to inbox | Medium |
Campaign delivered to multiple inboxes | Medium to High |
User clicked URL | High |
User clicked through warning | High |
Priority account clicked | High |
Click followed by suspicious sign-in | Critical |
This model should be adapted to each organization’s risk profile and response process.
Limitations and Things to Validate
Before using this approach in production, validate the following:
- Defender for Office 365 Plan 2 availability
- Campaign Views permissions
- CampaignInfo table availability
- Defender XDR connector configuration
- Advanced hunting event streaming
- Field names in your environment
- Retention period
- Data latency
- Join behavior using NetworkMessageId
- Whether click events can be joined to email metadata in all cases
One important limitation is that some URL click events may not join cleanly with email metadata. For example, clicks from Drafts or Sent Items may not have the same message metadata available for correlation.
Also, because CampaignInfo is currently documented as Preview, I would avoid depending on it alone for critical production automation without testing and validation.