One Service Principal Made 6,000 API Calls Before Breakfast
There was no suspicious user sign-in.
No impossible travel.
No MFA prompt.
No interactive session at all.
Instead, one application identity had become extraordinarily busy.
Before most of the SOC had started work, a single service principal had made more than 6,000 Microsoft Graph requests.
That number looked dramatic.
But Agent Foskett knew volume alone was not a verdict.
Was this expected automation — or had a workload identity become the attacker?

No User. Thousands of Requests.
Workload identities authenticate without a person sitting at a keyboard. Their behaviour still leaves evidence.
6,000 calls sounded suspicious — but was it?
So Agent Foskett did not begin with the assumption that high volume meant compromise.
He began with a baseline question.
Was this volume normal for this service principal?
Find the busiest service principals
AADServicePrincipalSignInLogs for service-principal sign-ins. Agent Foskett first ranked workload identities by recent authentication activity to identify whether the application was unusually active compared with its peers.- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
AADServicePrincipalSignInLogs
| where TimeGenerated > ago(1d)
| summarize SignInCount = count(),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated),
SourceIPs = make_set(IPAddress, 20)
by ServicePrincipalId,
ServicePrincipalName,
AppId
| order by SignInCount descAuthentication was only half the story
It does not automatically tell you every Microsoft Graph operation performed with the resulting token.
For that, Agent Foskett moved to Microsoft Graph activity telemetry.
Authentication answers how the workload got in. API activity answers what it did next.
Count the Microsoft Graph requests
MicrosoftGraphActivityLogs exposed HTTP requests made against tenant resources. Agent Foskett grouped the activity by application and service principal rather than assuming the 6,000-call headline was meaningful by itself.- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
MicrosoftGraphActivityLogs
| where TimeGenerated > ago(1d)
| summarize Requests = dcount(RequestId),
FirstRequest = min(TimeGenerated),
LastRequest = max(TimeGenerated),
Methods = make_set(RequestMethod, 10),
StatusCodes = make_set(ResponseStatusCode, 20)
by AppId,
ServicePrincipalId
| order by Requests descThe volume was new
Its request volume had jumped sharply compared with previous days.
The time window was unusual.
The endpoints were different.
And write operations appeared where the workload normally performed reads.
The anomaly was not simply “6,000”. It was 6,000 requests that did not look like this identity's normal job.
Find what the application was calling
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
let TargetServicePrincipal =
"00000000-0000-0000-0000-000000000000";
MicrosoftGraphActivityLogs
| where TimeGenerated > ago(1d)
| where ServicePrincipalId == TargetServicePrincipal
| extend ResourcePath =
tostring(parse_url(RequestUri).Path)
| summarize Requests = dcount(RequestId),
Successes = countif(ResponseStatusCode between (200 .. 299)),
Failures = countif(ResponseStatusCode >= 400)
by RequestMethod,
ResourcePath
| order by Requests descReads and writes changed the meaning of the investigation
POST, PATCH, PUT or DELETE activity against unexpected resources deserved a different level of attention.
Agent Foskett therefore separated volume from capability and capability from actual use.
What an application can do matters. What it actually did matters more.
Inspect successful write operations
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
let TargetServicePrincipal =
"00000000-0000-0000-0000-000000000000";
MicrosoftGraphActivityLogs
| where TimeGenerated > ago(1d)
| where ServicePrincipalId == TargetServicePrincipal
| where RequestMethod in ("POST", "PATCH", "PUT", "DELETE")
| where ResponseStatusCode between (200 .. 299)
| project TimeGenerated,
AppId,
ServicePrincipalId,
RequestMethod,
RequestUri,
ResponseStatusCode,
RequestId,
SignInActivityId,
Roles,
Scopes
| order by TimeGenerated ascPermissions explained possibility — not intent
Agent Foskett compared those claims with the application's intended purpose and its Entra configuration.
A powerful application permission could explain how the operation was authorised.
It could not explain why the operation happened.
Authorization tells you what the workload was allowed to do. Investigation tells you whether the behaviour made sense.
Correlate Graph activity with workload authentication
SignInActivityId and the sign-in log's UniqueTokenIdentifier to correlate API activity with authentication evidence where a matching record existed.- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
let TargetServicePrincipal =
"00000000-0000-0000-0000-000000000000";
MicrosoftGraphActivityLogs
| where TimeGenerated > ago(4d)
| where ServicePrincipalId == TargetServicePrincipal
| join kind=leftouter (
AADServicePrincipalSignInLogs
| where TimeGenerated > ago(4d)
| project SignInTime = TimeGenerated,
ServicePrincipalId,
ServicePrincipalName,
IPAddress,
ResourceDisplayName,
ResultType,
UniqueTokenIdentifier
)
on $left.SignInActivityId == $right.UniqueTokenIdentifier
| project TimeGenerated,
SignInTime,
ServicePrincipalName,
AppId,
ServicePrincipalId,
IPAddress,
RequestMethod,
RequestUri,
ResponseStatusCode,
SignInActivityId
| order by TimeGenerated ascNot every Graph request will necessarily have a matching sign-in row
Microsoft notes that some Microsoft Graph activity does not have a corresponding service-principal sign-in record, including some first-party app-only activity.
A missing join therefore was not automatically proof of log loss or evasion.
Correlation gaps need interpretation, not assumptions.
Now ask what changed
So why had its behaviour changed today?
Agent Foskett reviewed recent application-management events, credential changes, permission grants and ownership changes around the workload identity.
He also checked whether the source IPs and authentication pattern were consistent with the system that normally used the application.
A long-lived application can become dangerous when its credentials, permissions or operator change.
Do not confuse a service principal with a user
No MFA challenge to inspect in the normal user sense.
No desktop session to reconstruct.
The investigation had to follow workload evidence: application identity, credentials, permissions, source addresses, target resources and API operations.
Workload identities require workload investigation techniques.
Build the workload-identity timeline
What Agent Foskett checked
What the evidence can and cannot prove
AADServicePrincipalSignInLogs can show service-principal authentication activity when that telemetry is collected.MicrosoftGraphActivityLogs can show Microsoft Graph requests and fields such as application ID, service-principal ID, request URI, method and response status when Graph activity logging is enabled and routed to the workspace.High request volume alone does not prove compromise.
Successful API operations alone do not prove malicious intent.
The conclusion comes from behaviour, permissions, business purpose, authentication context and the actual resources touched.
The lesson
It had no mailbox.
No keyboard.
No human session.
But it had an identity, credentials and permission to call Microsoft Graph.
And once its behaviour changed, those permissions mattered.
Workload identities do not need to look human to become part of an attack.
Related investigations
Final thought
Users sign in.
Users click links.
Users run processes.
But applications authenticate too.
Service principals can hold powerful permissions and operate continuously without anybody sitting in front of a screen.
Six thousand API calls were not the answer.
They were the clue that told Agent Foskett where to start looking.
The Logs Already Knew! 🔎

