This hunt targets adversaries who compromise established AI agents by injecting unauthorized Model Context Protocol (MCP) servers to establish persistent command-and-control channels or exfiltrate sensitive data. Proactive hunting in Azure Sentinel is critical because these subtle configuration changes often bypass standard alerting thresholds, allowing attackers to operate undetected within trusted agent environments before initiating malicious activities.
let lookback = 14d;
let recent = 2d;
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);
let CurrentRaw = materialize(
AgentsInfo
| where Timestamp > ago(recent)
| summarize arg_max(Timestamp, *) by AgentId
| where LifecycleStatus != "Deleted");
let CurrentMcp =
CurrentRaw
| mv-expand Mcp = McpServers
| extend McpName = tostring(Mcp.name)
| where isnotempty(McpName)
| summarize CurrentMcpServers = make_set(McpName) by AgentId;
let BaselineRaw = materialize(
AgentsInfo
| where Timestamp between (ago(lookback) .. ago(recent))
| where LifecycleStatus != "Deleted"
| summarize arg_max(Timestamp, *) by AgentId);
let BaselineMcp =
BaselineRaw
| mv-expand Mcp = McpServers
| extend McpName = tostring(Mcp.name)
| where isnotempty(McpName)
| summarize KnownMcpServers = make_set(McpName) by AgentId;
let Baseline =
BaselineRaw
| join kind=leftouter BaselineMcp on AgentId
| extend BaselineMcpServers = coalesce(KnownMcpServers, dynamic([]))
| project AgentId, PreviousTimestamp = Timestamp, BaselineMcpServers;
CurrentRaw
| join kind=inner CurrentMcp on AgentId
| join kind=inner Baseline on AgentId
| extend AddedMcpServers = set_difference(CurrentMcpServers, BaselineMcpServers)
| where array_length(AddedMcpServers) > 0
| extend OwnerIds = iff(array_length(coalesce(Owners, dynamic([]))) > 0, Owners, dynamic([""]))
| mv-expand OwnerId = OwnerIds to typeof(string)
| join kind=leftouter IdentityIdtoUPN on $left.OwnerId == $right.AccountObjectId
| project-rename OwnerUpn = AccountUpn
| extend OwnerAccountName = tostring(split(OwnerUpn, "@")[0]),
OwnerAccountUPNSuffix = tostring(split(OwnerUpn, "@")[1])
| project Timestamp, PreviousTimestamp, AgentId, Name, Platform, CreatedDateTime,
AddedMcpServers, BaselineMcpServers, CurrentMcpServers, OwnerId, OwnerUpn,
OwnerAccountName, OwnerAccountUPNSuffix
| sort by Timestamp desc
id: d6ab015a-0a76-47f4-959f-54f49edff3f3
name: AI Agents - Newly observed MCP server on existing agent
description: |
Identifies MCP server names newly observed on an existing AI agent compared with its latest baseline snapshot. Review inventory and audit records to confirm whether the configuration change was authorized. Run within 2 days of a change to retain coverage.
requiredDataConnectors: []
tactics: []
relevantTechniques: []
query: |
let lookback = 14d;
let recent = 2d;
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);
let CurrentRaw = materialize(
AgentsInfo
| where Timestamp > ago(recent)
| summarize arg_max(Timestamp, *) by AgentId
| where LifecycleStatus != "Deleted");
let CurrentMcp =
CurrentRaw
| mv-expand Mcp = McpServers
| extend McpName = tostring(Mcp.name)
| where isnotempty(McpName)
| summarize CurrentMcpServers = make_set(McpName) by AgentId;
let BaselineRaw = materialize(
AgentsInfo
| where Timestamp between (ago(lookback) .. ago(recent))
| where LifecycleStatus != "Deleted"
| summarize arg_max(Timestamp, *) by AgentId);
let BaselineMcp =
BaselineRaw
| mv-expand Mcp = McpServers
| extend McpName = tostring(Mcp.name)
| where isnotempty(McpName)
| summarize KnownMcpServers = make_set(McpName) by AgentId;
let Baseline =
BaselineRaw
| join kind=leftouter BaselineMcp on AgentId
| extend BaselineMcpServers = coalesce(KnownMcpServers, dynamic([]))
| project AgentId, PreviousTimestamp = Timestamp, BaselineMcpServers;
CurrentRaw
| join kind=inner CurrentMcp on AgentId
| join kind=inner Baseline on AgentId
| extend AddedMcpServers = set_difference(CurrentMcpServers, BaselineMcpServers)
| where array_length(AddedMcpServers) > 0
| extend OwnerIds = iff(array_length(coalesce(Owners, dynamic([]))) > 0, Owners, dynamic([""]))
| mv-expand OwnerId = OwnerIds to typeof(string)
| join kind=leftouter IdentityIdtoUPN on $left.OwnerId == $right.AccountObjectId
| project-rename OwnerUpn = AccountUpn
| extend OwnerAccountName = tostring(split(OwnerUpn, "@")[0]),
OwnerAccountUPNSuffix = tostring(split(OwnerUpn, "@")[1])
| project Timestamp, PreviousTimestamp, AgentId, Name, Platform, CreatedDateTime,
AddedMcpServers, BaselineMcpServers, CurrentMcpServers, OwnerId, OwnerUpn,
OwnerAccountName, OwnerAccountUPNSuffix
| Sentinel Table | Notes |
|---|---|
IdentityInfo | Ensure this data connector is enabled |
Here are 5 specific false positive scenarios for the AI Agents - Newly observed MCP server on existing agent rule, tailored for an enterprise environment:
Scheduled Patching of Internal Knowledge Bases
02:00–04:00 UTC on Sundays) for known internal MCP servers like Enterprise-Wiki-MCP and HR-Policy-Repo. Additionally, filter alerts where the server name matches a regex pattern ending in -v* or includes version tags, indicating a routine upgrade rather than a new deployment.Onboarding of New Departmental Tools via Service Accounts
FinOps-Data-MCP server during its initialization sequence. Since this is part of a standard provisioning script executed by the svc-provisioning service account, it triggers the rule as an unexpected new observation before the baseline snapshot captures it.svc-provisioning, svc-automation) and correlate with Change Management tickets tagged as “Onboarding” or “Provisioning.” A filter can be applied to suppress alerts for the first 24 hours of a server-agent relationship if the agent’s creation timestamp is within that same window.**Autom