Forum Discussion

klianos's avatar
klianos
Iron Contributor
Jun 11, 2026

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:

  1. Start from Campaign Views in Microsoft Defender XDR.
  2. Identify the campaign details.
  3. Use CampaignInfo to list affected users and messages.
  4. Join with EmailEvents to validate delivery status.
  5. Join with UrlClickEvents to identify user interaction.
  6. Join with EmailPostDeliveryEvents to confirm remediation.
  7. Review related Microsoft XDR incidents in Microsoft Sentinel.
  8. 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.

No RepliesBe the first to reply