investigation
330 TopicsMicrosoft Defender AMSI false positive — private evidence submission guidance
We're seeking guidance on the official private review process for a suspected Microsoft Defender Antivirus AMSI false-positive detection. Detection: HackTool:PowerShell/ApexToolkit.A The detection occurred during an authorized PowerShell-based .NET development/testing workflow. The original AMSI payload is unavailable. We've collected the official Microsoft Defender diagnostic CAB, but it contains personal and internal system information, along with unrelated security history. Microsoft Support referred us to Tech Community because the consumer support channel could not escalate the investigation directly. Our question: Is there an official private channel for submitting sanitized incident records for initial classification review, or obtaining guidance about secure handling of the full diagnostic CAB? We want to maintain Defender protections and avoid posting sensitive diagnostic evidence publicly. Thank you for any guidance on the appropriate Microsoft review process.42Views0likes0CommentsWhat’s new in Microsoft Sentinel: September 2026
Welcome back to What's new in Microsoft Sentinel. In September, Case Management brings investigation and collaboration together in Microsoft Defender, with a shared workspace to track work through resolution. Defender unified RBAC custom roles for granular delegated admin privileges (GDAP) give you finer control over what managed security service providers (MSSPs) and other partners can access. In Sentinel data lake, a single Codeless Connector Framework (CCF) connector can bring in data from many accounts, and our connector catalog has passed 500 data connectors. Read on for the details. Sentinel innovations: Sentinel SIEM Sentinel data lake Sentinel SIEM Case Management (Public Preview) Starting September 23, 2026, Case Management brings investigation and collaboration together in one experience in Defender. With Incident Cases, you can investigate, coordinate response, assign work, capture notes, and track progress through resolution, all from a shared workspace that holds investigation context alongside tasks and workflow tracking. Your existing Sentinel automation, playbooks, workbooks, and integrations carry over unchanged. This builds on the integrated security operations (ISOC) vision of bringing the context and workflows practitioners need to investigate and respond together in Defender. Read our Case Management blog, see the documentation, and watch the demo to learn more. Unified RBAC custom roles (Public Preview) If you work with a managed security service provider (MSSP) or manage multiple tenants, giving partners the right level of access is simpler. Granular delegated admin privileges (GDAP) sets which partner groups can be delegated access to your tenant, and Defender unified role-based access control (RBAC) sets what they can see and do once delegated. You can assign unified RBAC custom roles to GDAP remote tenant groups and scope each role down to specific security products, data sources, and data collections so partners receive only the access they need. GDAP previously only supported Microsoft Entra directory roles, which often meant handing partners more access than the job needed. Anyone who is a member of a remote tenant group receives its permissions. Your partner manages who belongs to the group, while you keep control of the role and its scope. Learn more. Sentinel data lake Multi-account support for codeless connectors (Public Preview) A single connector built on the Codeless Connector Framework (CCF) can bring data from many accounts, tenants, or environments into one Sentinel workspace. A unified connections list shows each account as its own row, labeled so you can tell them apart by account name, tenant ID, or environment. An “Add connection” experience walks you through setting up each one, and an independent lifecycle per connection means you can edit or remove any account without affecting the rest. Shared, efficient ingestion sends all of it to the same table, so you have one place to investigate instead of a separate connector for every account. This helps if you run separate production and development accounts, segment by region or business unit, or manage several customers in one workspace. Learn more. Sentinel data connectors pass the 500 mark Sentinel has reached an exciting milestone of more than 500 data connectors. Our connectors bring security signals from Microsoft services, non-Microsoft solutions, multicloud environments, applications, infrastructure, and custom sources into Sentinel, where they can be correlated and analyzed to help teams detect threats, investigate incidents, hunt across their digital estate, and automate response. With 500+ connectors and growing, Sentinel makes it easier for organizations to unify their security ecosystem and give the security operations center (SOC) broader visibility where it matters most. Find your connector. Additional resources Blogs and documentation: Integrated Security Operations Center in Microsoft Defender ISOC in Microsoft Defender (preview) Activate Microsoft Defender unified role-based access control (URBAC) Support multiple connections in a codeless connector Upcoming webinars: Oct 6: Security Immersion Event: Into the Breach Oct 14: Microsoft Security Immersion Event: Shadow Hunter Upcoming events: Oct 19: Microsoft Security Days Summit: Dallas Edition (In Person) ISOC: What's new with the Defenders Capture the Flag: Agent Wars (Hands-on) Nov 17–20: Microsoft Ignite at Moscone Center in San Francisco, CA, USA. See what's next in Microsoft Security. Register now. Stay connected Check back each month for the latest innovations, updates, and events to ensure you’re getting the most out of Microsoft Sentinel. We’ll see you in the next edition!1.1KViews2likes1CommentRESYNC MICROSOFT DEFENDER FOR ENDPOINT
hey guys im a junior IT officer and i have been tasked with finding a way to get microsoft defender for enpoint to get fresh data without waiting 24hrs or the 4 hours that microsoft insists the resync will happen,I tried advanced hunting running queries directly there,i tried runnning an anti virus scan but nothing happened and APIs i tried looking for APIs that can initiate a restart but nothing...any ideas ? i read on here there was github repo for using powerBI with defender to get new data https://github.com/microsoft/MicrosoftDefenderForEndpoint-PowerBI but its not clear how that work...any intel would be helpful299Views0likes1CommentMonitoring work from home
My boss has asked me if there is a way to see just how "busy" people who are working from home are. I have this data in Sentinel: Entra sign-in logs Defender for Endpoint logs Office 365 logs Most, if not all, on premise AD login events Netskope (current ZTNA solution) logs I have my known office location IPs so i could just exclude those and look for activity from other IPs however many times people will work in the morning or on the way to work appearing from a non corporate IP, come into the office appearing to come from a corporate IP, and then from home again in the evening. I need a way to query Sentinel looking for people who appear to be working but not coming from Corp IP. If they came from different IPs on the same day including Corp check to see if those non Corp are before and after business hours and exclude those. Anyone know of a good query to achieve this? Or maybe a tool that can extract and generate a report?238Views0likes0CommentsHunting AI Agent Configuration Drift with Microsoft Sentinel
Four KQL patterns for detecting instruction changes, new MCP servers, ownership changes, and organization-wide sharing I recently authored and contributed four new Microsoft Sentinel hunting queries for detecting security-relevant configuration drift in AI agents. They have been reviewed, approved, and merged into Microsoft's public Azure-Sentinel repository. I built the queries around four changes that can materially affect an agent's behavior, access, or exposure: instructions being modified, MCP servers being connected, owners being added, and sharing being expanded to the entire organization. Each modification may be legitimate, but each deserves enough context for a security team to verify that it was expected and authorized. For a security operations team, the difficult question is often not what does this agent look like now? It is what changed since the last known state? Microsoft Sentinel's AgentsInfo table provides inventory-style snapshots of AI agents and their associated configuration. That makes it useful for more than posture reporting. By comparing a recent snapshot with an earlier baseline, we can hunt for configuration drift that deserves investigation. This post walks through four practical hunting scenarios: Instructions changed on a previously published agent A newly observed MCP server on an existing agent An owner added to an MCP-enabled agent Sharing expanded from a restricted scope to the entire organization The complete hunting queries are available in Microsoft's public https://github.com/Azure/Azure-Sentinel/tree/master/Hunting%20Queries/AI%20Agents. The focus here is the detection design behind them, the KQL patterns they share, and the investigation questions they help answer. What I contributed I wrote the four standalone hunting queries discussed in this article and submitted them to Azure/Azure-Sentinel in https://github.com/Azure/Azure-Sentinel/pull/14702: AI Agents - Instructions changed on previously published agent AI Agents - Newly observed MCP server on existing agent AI Agents - Owner added to MCP-enabled agent AI Agents - Sharing expanded to organization-wide The contribution went through several rounds of technical review. Across six commits, I aligned the queries with the unified AgentsInfo schema, added schema-tolerant IdentityInfo enrichment, improved entity mappings, bounded the identity lookback, expanded all owner values, and kept the ATT&CK mappings limited to scenarios where a precise technique could be defended. Repository collaborator v-atulyadav approved the final revision, and the four queries were merged into master on July 20, 2026. This article explains the detection logic and engineering decisions behind that contribution rather than simply reproducing the final YAML files. Why current-state queries are not enough A current-state query can answer questions such as: Which agents are published? Which agents have MCP servers configured? Which agents are shared with the organization? Who owns a particular agent? Those are important posture questions, but they do not tell us whether the state is new. An agent with an MCP server might have been reviewed and approved months ago. The same MCP server appearing for the first time today is a different security signal. Configuration-drift hunting adds the missing time dimension. Instead of treating a risky-looking property as an event, it compares two states of the same agent and reports only meaningful transitions. The common detection pattern I used the same basic time model across all four hunts: let lookback = 14d; let recent = 2d; The latest snapshot observed during the last two days becomes the current state. The latest snapshot from the preceding portion of the 14-day lookback becomes the baseline. Conceptually, the comparison looks like this: let CurrentState = AgentsInfo | where Timestamp > ago(recent) | summarize arg_max(Timestamp, *) by AgentId | where LifecycleStatus != "Deleted"; let BaselineState = AgentsInfo | where Timestamp between (ago(lookback) .. ago(recent)) | where LifecycleStatus != "Deleted" | summarize arg_max(Timestamp, *) by AgentId; CurrentState | join kind=inner BaselineState on AgentId Several details matter here: arg_max(Timestamp, *) by AgentId selects the latest available state for each agent in the relevant time range. The inner join restricts results to agents that exist in both periods. A newly created agent is therefore not automatically treated as configuration drift on an existing agent. Deleted lifecycle snapshots are excluded so that a deletion record does not become the effective baseline or current configuration. The two-day current window is operationally significant. To retain coverage, these hunts should run within two days of a change. The 14-day and two-day values are practical defaults, not universal constants. Environments with different ingestion cadence or retention requirements can adjust them, but the current and baseline windows must remain non-overlapping. Scenario 1: Instructions changed on a published agent An agent's instructions define its default behavior, persona, and operating boundaries. Changing them can be part of normal development, but it can also weaken restrictions, redirect the agent's behavior, or modify how it uses connected capabilities. The first hunt compares the current and previous instruction values only when the agent was published in both snapshots: CurrentState | join kind=inner BaselineState on AgentId | where CurrentInstructions != PreviousInstructions | extend PreviousInstructionsHash = hash_sha256(PreviousInstructions), CurrentInstructionsHash = hash_sha256(CurrentInstructions), InstructionsLengthDelta = strlen(CurrentInstructions) - strlen(PreviousInstructions) I deliberately chose to expose hashes and a length delta rather than returning both instruction bodies in plaintext. This confirms that a change occurred without unnecessarily spreading potentially sensitive prompts through query results, exports, or screenshots. Useful investigation questions include: Was the change associated with an approved development or release process? Did the agent remain published while the instructions changed? Were guardrails, declared tools, permissions, or sharing settings modified around the same time? Do audit records identify an expected actor and change path? The query maps well to an integrity-focused investigation. Its MITRE ATT&CK mapping is T1565.001 (Stored Data Manipulation), but the result is still a hunting lead rather than proof of malicious manipulation. https://github.com/Azure/Azure-Sentinel/blob/master/Hunting%20Queries/AI%20Agents/AgentsInfoInstructionsChangedOnPublishedAgent.yaml Scenario 2: A newly observed MCP server Model Context Protocol servers can extend an agent with external tools, data sources, or actions. From a defender's perspective, the important transition is not simply that an MCP server exists. It is that a server name appears in the current configuration but was absent from the baseline. The query expands the dynamic McpServers array and builds a set of server names for each agent: let CurrentMcp = CurrentRaw | mv-expand Mcp = McpServers | extend McpName = tostring(Mcp.name) | where isnotempty(McpName) | summarize CurrentMcpServers = make_set(McpName) by AgentI It performs the same normalization for the baseline, then calculates the difference: | extend AddedMcpServers = set_difference(CurrentMcpServers, BaselineMcpServers) | where array_length(AddedMcpServers) > 0 Using set_difference() avoids raising a result merely because the order of array elements changed. The hunt reports only MCP server names present in the current set and absent from the previous set. An analyst should validate more than the displayed name: Is the MCP integration part of the approved inventory? What endpoint, authentication method, and permissions are associated with it? Which tools or data can the server expose to the agent? Was the integration introduced through an expected deployment path? Did ownership, instructions, or sharing change in the same period? I did not assign an ATT&CK technique to this query. Adding an MCP server does not, by itself, prove command execution, persistence, or a specific attacker behavior. Avoiding an overly broad mapping keeps the signal honest. https://github.com/Azure/Azure-Sentinel/blob/master/Hunting%20Queries/AI%20Agents/AgentsInfoNewlyObservedMcpServer.yaml Scenario 3: An owner added to an MCP-enabled agent Ownership is a control-plane relationship. A newly added owner may be able to modify an agent's configuration, instructions, integrations, or publication state. The risk becomes more interesting when the agent already has MCP servers configured. The hunt first limits the current state to MCP-enabled agents: | where array_length(coalesce(McpServers, dynamic([]))) > 0 | project AgentId, Timestamp, Name, Platform, CreatedDateTime, CurrentOwners = coalesce(Owners, dynamic([])), McpServers It then compares the owner arrays as sets: | extend AddedOwners = set_difference(CurrentOwners, PreviousOwners) | where array_length(AddedOwners) > 0 | mv-expand AddedOwnerId = AddedOwners to typeof(string) Expanding AddedOwners produces one row per newly observed owner. This is more useful than returning one opaque dynamic array because every added identity can be enriched, mapped, and investigated independently. I kept the raw object identifier in the result even when identity enrichment fails: | extend AddedOwnerUpn = AccountUpn, UnresolvedAddedOwnerId = iff(isempty(AccountUpn), AddedOwnerId, "") That fallback matters. A missing UPN should not hide the underlying ownership change. Investigation should establish: Is the added owner an expected person, service identity, or administrative group? Does the identity's role and business function justify control of this agent? Was the owner added before other configuration changes? Does the identity appear in related sign-in, audit, or privileged-access activity? Should ownership be removed while the change is reviewed? This query maps to T1098 (Account Manipulation) under Persistence and Privilege Escalation. As with the instruction-change hunt, the mapping frames an investigation hypothesis; it does not label every ownership change as malicious. https://github.com/Azure/Azure-Sentinel/blob/master/Hunting%20Queries/AI%20Agents/AgentsInfoOwnerAddedToMcpAgent.yaml Scenario 4: Sharing expanded to the entire organization An agent can move from a limited audience to organization-wide availability without changing its underlying tools or instructions. That transition can materially increase exposure, especially when the agent has MCP integrations or declared tools. The hunt treats "*" in SharedWith as the organization-wide state. The current snapshot must contain it, while the baseline must not: // Current state | where set_has_element(coalesce(SharedWith, dynamic([])), "*") // Baseline state | where not(set_has_element(coalesce(SharedWith, dynamic([])), "*")) The result also counts MCP servers and declared tools: | extend McpServerCount = array_length(coalesce(McpServers, dynamic([]))), DeclaredToolCount = array_length(coalesce(DeclaredTools, dynamic([]))) | extend HasElevatedCapabilities = McpServerCount > 0 or DeclaredToolCount > 0 | sort by HasElevatedCapabilities desc, Timestamp desc I use HasElevatedCapabilities as a prioritization field, not a verdict. It brings agents with connected capabilities to the top of the result set so analysts can review the potentially larger blast radius first. Questions for triage include: Was organization-wide publication explicitly approved? Is the agent intended for every user, or was a group-based scope expected? What data sources, tools, and MCP servers can organization-wide users reach through it? Do the instructions contain assumptions that were safe only for a restricted audience? Were access reviews or user-acceptance tests completed before the expansion? No ATT&CK mapping is assigned because a broader sharing scope is a security-relevant exposure change, but not a sufficiently precise adversary technique on its own. https://github.com/Azure/Azure-Sentinel/blob/master/Hunting%20Queries/AI%20Agents/AgentsInfoSharingExpandedToOrgWide.yaml Resolving owners without making the hunt schema-fragile The Owners field contains identifiers. Human-readable identity context makes results easier to triage, and entity mappings make those identities more useful in Sentinel investigations. I built a small, materialized IdentityInfo lookup that is shared by the four hunts: let IdentityIdtoUPN = materialize( IdentityInfo | extend ResolvedAccountUpn = tostring( column_ifexists("AccountUpn", column_ifexists("AccountUPN", ""))), IdentityTimestamp = todatetime( column_ifexists("Timestamp", column_ifexists("TimeGenerated", datetime(null)))) | where IdentityTimestamp >= ago(lookback) | where isnotempty(AccountObjectId) and isnotempty(ResolvedAccountUpn) | summarize arg_max(IdentityTimestamp, ResolvedAccountUpn) by AccountObjectId | project AccountObjectId = tostring(AccountObjectId), AccountUpn = ResolvedAccountUpn); There are three design choices worth noting: column_ifexists() accommodates observed IdentityInfo naming variants without maintaining separate query versions. The lookup is bounded by the same lookback period instead of scanning unbounded identity history. arg_max() keeps the latest usable identity record for each object ID. After enrichment, the queries map the account using the UPN components and the Entra object ID: entityMappings: - entityType: Account fieldMappings: - identifier: Name columnName: OwnerAccountName - identifier: UPNSuffix columnName: OwnerAccountUPNSuffix - identifier: AadUserId columnName: OwnerId The strong AadUserId identifier remains valuable even when display information changes. Microsoft Sentinel can use mapped entities in bookmarks and investigation experiences, so mapping the changed owner is more than cosmetic enrichment. Turning a result into an investigation These queries intentionally stop at the configuration transition. AgentsInfo tells us that two snapshots differ; it does not necessarily tell us who performed the change, through which interface, or whether the action was authorized. A practical investigation workflow is: Confirm that the two snapshots represent the expected agent and time period. Review the exact changed property and the agent's current capabilities. Identify the owner or newly added owner through IdentityInfo and Entra ID context. Correlate the transition with the relevant audit source for actor attribution. Check for related changes to permissions, tools, data sources, publication state, and sharing. Validate the change against an approved request, release, or ownership process. Restrict, unpublish, or revert the agent if the exposure cannot be justified. Expected changes can still be useful findings. Repeated legitimate results may reveal that a deployment process lacks a stable change window, that ownership is managed through noisy automation, or that the hunt's timing needs to be aligned with release activity. Tuning the hunts for your environment Before operational use, consider the following adjustments: Run cadence: Execute within the two-day current window. A daily cadence provides overlap without blending current and baseline periods. Lookback: Increase the 14-day lookback only if snapshot history and query cost support it. A longer lookback does not compensate for missing the current window. Known change windows: Add watchlists or environment-specific suppression logic for well-controlled automated deployments, while retaining enough context to audit the change. Agent scope: Filter by platform, business unit, agent naming convention, or owner if different teams require separate triage queues. Risk prioritization: Raise agents with sensitive declared data sources, powerful tools, privileged owners, or broad availability to the top of the result set. Audit correlation: Keep attribution logic separate unless the audit source and join keys are stable in your environment. This makes the configuration-drift hunt reusable while allowing each organization to attach its own control-plane evidence. Test with representative snapshots before treating any hunt as an operational control. In particular, validate array shapes for Owners, McpServers, and SharedWith, confirm the identity fields present in your workspace, and exercise both changed and unchanged states. Using the queries The four YAML definitions have been merged into the Hunting Queries/AI Agents folder of Microsoft's Azure-Sentinel repository. Each file contains the complete KQL, description, entity mappings, and ATT&CK mappings where a precise technique applies. https://github.com/Azure/Azure-Sentinel/blob/master/Hunting%20Queries/AI%20Agents/AgentsInfoInstructionsChangedOnPublishedAgent.yaml https://github.com/Azure/Azure-Sentinel/blob/master/Hunting%20Queries/AI%20Agents/AgentsInfoNewlyObservedMcpServer.yaml https://github.com/Azure/Azure-Sentinel/blob/master/Hunting%20Queries/AI%20Agents/AgentsInfoOwnerAddedToMcpAgent.yaml https://github.com/Azure/Azure-Sentinel/blob/master/Hunting%20Queries/AI%20Agents/AgentsInfoSharingExpandedToOrgWide.yaml The broader pattern is reusable beyond these four scenarios: select a stable current snapshot, select a non-overlapping baseline, normalize dynamic properties into comparable sets, calculate the transition, and enrich only after the drift has been identified. That keeps the core detection explainable and gives the analyst the before-and-after context needed for a defensible investigation. References https://learn.microsoft.com/en-us/azure/azure-monitor/reference/tables/agentsinfo https://learn.microsoft.com/en-us/azure/azure-monitor/reference/queries/agentsinfo https://learn.microsoft.com/en-us/azure/azure-monitor/reference/tables/identityinfo https://learn.microsoft.com/en-us/azure/sentinel/entities-reference https://github.com/Azure/Azure-Sentinel/pull/14702 I authored the four hunting queries discussed in this article and contributed them to Microsoft's Azure-Sentinel repository as https://github.com/Azure/Azure-Sentinel/pull/14702. The complete implementations and review history are publicly available through the links above.917Views1like2CommentsSentinel - Defender XDR KQL Queries Library
Hello all, I’ve been building something over the past few weeks that I think the security community might find useful. https://goxdr.fyi is a searchable KQL query library for Microsoft Sentinel and Defender XDR. The name comes from a nickname my colleagues gave me (GoX) combined with XDR. I also picked up https://goxdr.fyi as a short and easy to remember domain for it. You can check it out here: https://goxdr.fyi The idea came from my own day to day work as someone working in IAM and SOC operations. I constantly find myself writing and refining KQL queries for threat hunting, detection engineering and incident investigation. Over time I realized I had a growing collection of queries that I kept going back to and I thought why not make these available to others? It currently has 117 queries covering identity security, BEC/AiTM detection, NTLM and LDAP attack hunting, OAuth governance, AI/Copilot security, Sentinel alert trending, SOC performance metrics and more. Some of these queries are ones I wrote from scratch based on real scenarios I encountered in production environments. Others are community queries I tested and validated in my own setup. Only the ones I found genuinely useful and that actually worked against real data made it in. Each query comes with a description explaining what it detects and why it matters, along with severity levels, platform tags (Sentinel, XDR or both) and a copy button so you can paste it directly into Advanced Hunting or use it as the basis for an Analytics Rule. The site is open source, hosted on GitHub Pages and licensed under CC BY 4.0. No sign-up, no paywall, no tracking. The source is available. I’ll keep adding queries as new scenarios come up. If there’s enough interest I’m also considering adding Cortex XQL queries for Palo Alto environments. Suggestions, feedback or ideas for new detections are always welcome. Feel free to reach out. Thanks327Views0likes0CommentsDefender of XDR - Quarantine - Lack of filter/search options
Hi Microsoft, I love what you're doing with the Defender XDR portal, but could you please show some love to the Quarantine section soon? On a daily basis, I have to review emails caught in quarantine for false positives, and the lack of search and filtering options is appalling. As a company based in Denmark, 99% of legitimate emails come from .dk domains. Yet there is no way to search for or filter on something this simple. If I type .dk into the search box, I get 0 results, even though I can clearly see .dk sender addresses on the page. The filter options only allow me to enter full sender or recipient email addresses, which is of course almost useless in a quarantine-review context. Some examples of filters that would be extremely useful: Sender domain ends with .dk Sender domain contains .dk URL domain filtering Attachment name filtering Saved filter views More flexible search across message properties The Quarantine experience could be made dramatically better with relatively little effort. So please, pretty please, give the Quarantine portal some attention. It's often the part of Defender that security teams interact with every single day.621Views0likes4CommentsMicrosoft Defender Incident – Handling incident severity change
There's no dedicated history/audit endpoint for field-level transitions (like "this incident went from Low → High at timestamp X") in the /security/incidents Graph API — the incident object only exposes the current severity plus a lastUpdateDateTime, not a change log. So this isn't something you're missing; it genuinely doesn't exist as a queryable history today. Also worth knowing before you build around it: Graph change notifications (webhooks) are not documented as supported for /security/incidents — subscription/webhook support is only documented for the legacy /security/alerts resource, and that resource is deprecated with removal expected around April 2026. So polling is currently the only supported pattern for incidents specifically, not a limitation of your approach — there's no webhook alternative to fall back to yet. Given that, the fix is in your polling strategy, not in finding a hidden feature: instead of filtering once at creation time and then ignoring the incident, poll using $filter=lastUpdateDateTime gt {last_poll_timestamp}. Since lastUpdateDateTime bumps on any property change — including a severity escalation — this catches incidents that started as Low/Informational and later got escalated, without re-fetching everything. A pattern that works well in practice: GET /security/incidents?$filter=lastUpdateDateTime gt {last_poll_time}&$orderby=lastUpdateDateTime asc Then in your own store, diff the incoming severity against what you last recorded for that id to detect the transition yourself — you're effectively reconstructing the history client-side since the API won't give it to you natively. Store (incidentId, severity, lastUpdateDateTime) on each poll and compare. One gotcha: this still won't tell you the exact moment the severity changed if multiple fields changed between polls — only that it changed sometime between your last two poll timestamps. If you need second-level precision on transition timing, you'd need to poll more frequently (your 5-minute interval is probably fine for SOC triage purposes, but not for precise SLA timestamping).369Views0likes0CommentsCampaign-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.445Views0likes0Comments