The Classic-Agent Blind Spot: Detecting Non-Human Identities in Microsoft Sentinel
Microsoft Build 2026 made the direction official. The industry is moving from the app era into the agent era, and a meaningful share of the security keynote went to protecting agent identities across their full lifecycle - discovery, governance, conditional access, identity protection, audit. On the identity side the centerpiece is Microsoft Entra Agent ID, now generally available, which gives AI agents first-class identities and extends the familiar Entra controls to them.
For agents you build the new way, that is a complete story. For the ones you already run, it is not. And the gap is where most SOCs will get hurt first.
This post adapts a TechCommunity discussion I started in June 2026 into a longer write-up. Two Microsoft Sentinel detections you can deploy this week, the diagnostic switch most tenants quietly leave off, and the tuning that decides whether either rule survives in production. If you want to discuss in the Microsoft Sentinel community, head over to the original thread.
TL;DR
- The gap: Entra Agent ID covers modern agents that hold an Agent ID. The classic ones - ordinary service principals, app registrations, pre-platform Copilot Studio bots - sit outside Identity Protection for Agents and Conditional Access for Agents.
- The fix in the meantime: two KQL detections. A new credential added to a service principal (MITRE T1098.001), and a successful service principal sign-in from a previously unseen IP (MITRE T1078.004).
- The diagnostic switch:
ServicePrincipalSignInLogsandManagedIdentitySignInLogsare not streamed by default. Enable the categories in Entra Diagnostic Settings before anything else, or Detection 2 has no data to learn from. - The endgame: shrink the classic-agent population by migrating to Agent ID. These rules are the safety net until that work is done, and a permanent backstop for whatever never moves.
What Modern Agents Look Like, and Why Yours Probably Aren’t
Entra Agent ID draws a hard line between two populations.
A modern agent is created through the Agent ID platform and backed by an agent identity blueprint. It carries a proper Agent ID, leaves a full audit trail, and inherits the protection stack: Conditional Access for Agents (Entra ID P1), Identity Protection for Agents (Entra ID P2), and Identity Governance for Agents. Identity Protection in particular establishes a behavioral baseline per agent and flags anomalies automatically - the same engine that watches risky users, pointed at risky agents.
A classic agent is everything else. The service principal behind your deployment pipeline that calls Graph with a client secret. The app registration brokering a non-Microsoft integration. The Copilot Studio agent that existed before Agent ID was switched on in your tenant. Anything home-grown that holds client credentials and calls into the directory. In the Entra agent registry these all show up with Has Agent ID: No, and that flag is load-bearing - the new platform protections apply to identities that hold an Agent ID, not to identities that merely behave like agents.
Here is the uncomfortable part. The non-human identities your tenant already runs almost always outnumber the human accounts. They have no MFA in any meaningful sense - a client secret or a certificate is a password with worse rotation hygiene. A fresh credential added to a service principal does not surface in the user-facing portal the way a new sign-in factor on a user account does. And until the agent in question moves over to Agent ID, none of that activity gets the Identity Protection treatment.
The SOC job here is narrow and specific: detect risky behavior on the classic population, the one the new platform does not yet protect. Sentinel has the telemetry to do it. The switches just have to be in the right position.
What Sentinel Sees, and What It Doesn’t By Default
Three tables carry most of the signal.
AADServicePrincipalSignInLogsrecords service principal authentications - the client-credentials sign-ins that pipelines, agents, and automation actually use. No user context, no MFA, just an application proving it holds a secret or certificate.AADManagedIdentitySignInLogsdoes the same for managed identities, where Azure holds and rotates the credential for you.AuditLogsis the Entra directory change feed. This is where credential additions to service principals appear, alongside every other directory change.
One operational warning before any of the detection work pays off. The service principal and managed identity sign-in log categories are not streamed by default. They have to be enabled explicitly in the Entra Diagnostic Settings feeding your workspace, by name: ServicePrincipalSignInLogs and ManagedIdentitySignInLogs. Plenty of teams write the rule, deploy it, never check, and never notice the source table is empty. Verify ingestion before anything else.
In the Microsoft Entra admin center the path is Entra ID > Monitoring & health > Diagnostic settings. Edit the setting that targets your Sentinel-backed Log Analytics workspace and select the two categories. If you only do one, service principal sign-ins is the higher-value one - it gates Detection 2 below.
Detection 1: A New Credential on a Service Principal
Adding a fresh secret or certificate to an existing service principal is one of the cleanest persistence techniques in a Microsoft cloud. The attacker compromises a privileged user or app, drops a new credential on a service principal that already holds useful Graph permissions, and walks away with access that survives password resets and session revocation on the original account. It maps to MITRE T1098.001 - Account Manipulation: Additional Cloud Credentials. For a classic agent it is especially nasty: there is no Identity Protection baseline watching the service principal itself, so this audit event is the cleanest place to catch the move.
// Detection 1: new secret or certificate added to an application or service principal
// MITRE T1098.001 - Account Manipulation: Additional Cloud Credentials
AuditLogs
| where OperationName has_any ("Add service principal", "Certificates and secrets management")
| where Result =~ "success"
| extend Initiator = coalesce(
tostring(InitiatedBy.user.userPrincipalName),
tostring(InitiatedBy.app.displayName))
| extend InitiatorIp = tostring(InitiatedBy.user.ipAddress)
| mv-apply Target = TargetResources on (
where Target.type =~ "Application"
| extend TargetName = tostring(Target.displayName),
TargetId = tostring(Target.id),
KeyChanges = Target.modifiedProperties
)
| mv-apply Prop = KeyChanges on (
where tostring(Prop.displayName) =~ "KeyDescription"
| extend NewKeys = parse_json(tostring(Prop.newValue)),
OldKeys = parse_json(tostring(Prop.oldValue))
)
| extend AddedKeys = set_difference(NewKeys, OldKeys)
| where array_length(AddedKeys) > 0
| project TimeGenerated, Initiator, InitiatorIp, TargetName, TargetId, AddedKeys
| order by TimeGenerated desc
The OperationName filter catches the three shapes this event takes in the audit feed: Add service principal, Add service principal credentials, and Update application - Certificates and secrets management. The mv-apply over modifiedProperties isolates the KeyDescription change, and set_difference(NewKeys, OldKeys) confirms a key was actually added rather than removed - rotating an old credential out, by itself, does not fire the rule.
False positives split into two clean groups. Routine credential rotation happens on a schedule and tends to be initiated by a known automation account. CI/CD pipelines and infrastructure-as-code that provision app credentials do the same thing on every deploy. In both cases the initiator is the discriminant - allow-list the expected initiators, not the targets. A credential added by your deployment pipeline’s service account at the usual time is routine. The same change initiated by an interactive admin out of hours, or by an account that never normally touches app credentials, is what you want surfaced.
Detection 2: A First-Seen IP for a Service Principal
A service principal that has only ever authenticated from your Azure regions and suddenly signs in from somewhere new is a strong signal that its credential has been lifted and is being used elsewhere. Service principals have stable, boring network behavior, which makes a first-seen IP a far cleaner indicator for them than it is for roaming human users. This is exactly the behavioral baseline Identity Protection gives you for free on modern agents - rebuilt in KQL for the classic ones it ignores. MITRE T1078.004 - Valid Accounts: Cloud Accounts.
// Detection 2: classic-agent service principal signing in from a previously unseen IP
// MITRE T1078.004 - Valid Accounts: Cloud Accounts
let baseline = 14d;
let detection = 1d;
let KnownIPs =
AADServicePrincipalSignInLogs
| where TimeGenerated between (ago(baseline + detection) .. ago(detection))
| where tostring(ResultType) == "0"
| summarize KnownIPSet = make_set(IPAddress) by AppId;
AADServicePrincipalSignInLogs
| where TimeGenerated > ago(detection)
| where tostring(ResultType) == "0"
| lookup kind=leftouter KnownIPs on AppId
| where set_has_element(KnownIPSet, IPAddress) == false
| summarize FirstSeen = min(TimeGenerated),
Resources = make_set(ResourceDisplayName, 10)
by ServicePrincipalName, AppId, IPAddress
| order by FirstSeen desc
The query builds a per-application baseline of source IPs over the previous 14 days, then flags any successful sign-in in the most recent 24 hours from an address outside that set. ResultType == "0" is the success filter on AADServicePrincipalSignInLogs - anything else is a failure code, and you do not want failed sign-ins seeding the baseline of “known” addresses, otherwise an attacker probing your tenant would teach the rule to accept their IP.
The Tuning That Decides If These Rules Live or Die
Both rules work on day one and both are too noisy to keep on day two unless you tune them. The patterns that bite, in roughly the order you will hit them:
- Brand-new service principals have no baseline. Detection 2 surfaces them the moment they first sign in, because the
KnownIPSetis empty. That first event is usually worth one analyst glance. If it becomes a flood, excludeAppIdentries younger than the baseline window from the alert path and re-evaluate them after they have had 14 days to settle. - Cloud egress shifts addresses. If your agents leave through dynamic cloud IP ranges or shared NAT pools, exact-IP comparisons will chase your own infrastructure. Switch the comparison from
IPAddresstoAutonomousSystemNumber, or maintain a watchlist of known egress ranges that the rule consults before alerting. - Microsoft’s own first-party endpoints rotate. Service principals authenticating to Microsoft-owned resources sometimes show new IPs because the Microsoft side rebalanced, not because anything you control changed. Tag those at the watchlist level rather than triaging them per-incident.
- CI/CD pipelines are agents too. Detection 1 will fire on every routine rotation done by a pipeline service account if you do not allow-list the initiator. The signal worth keeping is human admins or unexpected accounts adding credentials, not the daily rotation script doing its job.
mv-applycardinality.TargetResourcesarrays are heavy on some audit events. The query above filters toTarget.type =~ "Application"early on purpose - keep that filter at the top of any future variation, otherwise themv-applycost grows quickly on a high-volume tenant.
Why These Are Stopgap Detections
The endgame is not to run these rules forever. It is to shrink the population they apply to. Inventory the agents that show Has Agent ID: No in the Entra registry, prioritize the ones holding sensitive Graph permissions, and bring them into the Agent ID platform. Once an agent has an Agent ID, Identity Protection for Agents and Conditional Access for Agents take over the baselining and policy enforcement you are doing here by hand.
A few things will not be solved by detection rules in the SIEM, no matter how clean the tuning gets:
- You cannot baseline what you do not ingest. If the service principal sign-in category is off, Detection 2 has nothing to learn from. Diagnostic settings are the gate, not the detection.
- Identity-Protection-style risk fusion is not free in KQL. The platform-side detections combine sign-in risk, token risk, and behavioral signals the audit feed does not directly expose. KQL gets you the loud anomalies. The subtler ones stay platform-only.
- Some classic agents will never migrate. Third-party integrations you do not own, legacy automation tied to specific app object IDs, anything where rotating the identity carries operational blast radius - these become permanent residents of the rule set. Plan for it and budget the tuning time.
- Incident triage still needs criticality context. A credential added to “some service principal” is a different incident from a credential added to one that has admin on production. If you have an exposure-management surface that tags critical assets (see Wiring Microsoft Security Exposure Management Into Sentinel for the pattern), wire the watchlist into the detection’s enrichment path so analysts see asset weight at incident open, not after.
What I’d Do This Week
In order, smallest first:
- Enable
ServicePrincipalSignInLogsandManagedIdentitySignInLogsin the Entra diagnostic setting that feeds your Sentinel workspace. Without those categories, the rest is theater. Verify withAADServicePrincipalSignInLogs | take 1an hour after enabling. - Deploy Detection 1. Low volume, high signal, modifiedProperties parsing is already done. Allow-list the obvious automation initiators, leave the rule open on everything else, and treat the first week of hits as a tuning loop rather than incidents.
- Run a one-off historical sweep. Same
OperationNamefilter againstAuditLogsover the last 90 days. Pull every service principal that had a credential added and cross-reference with whatever criticality signal you have. That list alone tends to be more interesting than people expect - most tenants surface a few credential changes nobody can immediately account for.
Detection 2 follows in week two, after the 14-day baseline has had time to fill with real-world traffic. Build the inventory of Has Agent ID: No service principals in parallel and start the Agent ID migration on the ones holding the most sensitive Graph permissions.
If you have a sharper variant of either rule - especially anything that handles cloud-egress address churn without losing signal - I would like to see it. Reach me at marcel@graewer.com.
References
- Microsoft TechCommunity - original discussion
- Microsoft Learn - What is Microsoft Entra Agent ID?
- Microsoft Learn - What’s new in Microsoft Entra Agent ID (GA)
- Microsoft Learn - Identity Protection for Agents
- Microsoft Learn - AADServicePrincipalSignInLogs reference
- Microsoft Learn - AuditLogs reference
- Microsoft Learn - Service principal sign-in logs
- Microsoft Learn - Configure Microsoft Entra diagnostic settings
- MITRE ATT&CK - T1098.001 Account Manipulation: Additional Cloud Credentials
- MITRE ATT&CK - T1078.004 Valid Accounts: Cloud Accounts

Security architecture and incident response by day, open-source security tooling by night. Writing about Microsoft Sentinel, detection engineering and what AI in security actually delivers.
Related Posts
Wiring Microsoft Security Exposure Management Into Sentinel - Triage with Asset Criticality and Attack-Path Context
MSEM gives Sentinel something it never had: asset criticality and attack-path context per entity. Architecture, KQL patterns for incident enrichment, and the entity-matching pitfalls that quietly break them.
RoguePlanet: The Fourth Defender Zero-Day, and Why the Patch Is Not the End of It
CVE-2026-50656 is patched, but the fix ships as an engine update, not a Patch Tuesday KB. What RoguePlanet does, how it rhymes with BlueHammer, and the retrospective hunt and patch validation blue teams still owe themselves.