On January 21, 2026, NexGen Energy’s SOC received alerts for suspicious activity across Microsoft Entra ID and Azure. A Finance department employee interacted with what appeared to be a legitimate internal email, triggering unusual OAuth consent activity and unauthorised access patterns across multiple accounts and Azure resources. Investigation reveals a full multi-stage cloud intrusion attributed to Storm-0558.
All log sources in this lab are ingested as Custom Logs with a _CL suffix — AuditLogs_CL, SigninLogs_CL, OfficeActivity_CL, AzureActivity_CL, and AzureDiagnostics_CL. The first thing I did was orient myself with a table inventory:
search *
| summarize count() by Type
| order by count_ desc
This confirmed all five tables were populated with data. One lesson learned early: the _CL schema field names don’t always match what you’d expect from native Sentinel tables. I lost time on Q6 because I ran an external IP lookup on 51.89.156.153, got France (OVH SAS, Roubaix), and couldn’t understand why the lab rejected the answer. The lab has Beijing, CN hardcoded in the SigninLogs location field. The SIEM is the source of truth, not ipinfo. Two habits that would have saved significant time here: always check the full row schema with take 1 before building filters, and always copy-paste answers directly from query output rather than retyping.
The attack chain starts in AuditLogs_CL. Filtering for Email sent activity ordered by ActivityDateTime ascending surfaces the initial phishing email:
AuditLogs_CL
| where ActivityDisplayName == "Email sent"
| project ActivityDateTime, InitiatedByUser, InitiatedByUserPrincipalName, InitiatedByIpAddress, TargetResources
| order by ActivityDateTime asc

The sender is david@nexgenenrgy.com — a typosquat of the legitimate domain, missing the second e. The email landed in marcus.reid@nexgenenergy.com’s inbox at 01:38 AM with subject “URGENT: Verify Your Budget Planning App Access.” The urgency framing is deliberate — designed to push the target to act without scrutiny. The sending IP is 51.89.156.153, the attacker’s primary IP throughout the investigation.
Marcus interacted with the phishing link and granted consent to a malicious OAuth application. Filtering for consent activity confirms it:
AuditLogs_CL
| where ActivityDisplayName has_any ("consent", "Consent to application", "Add delegated permission grant")
| project ActivityDateTime, InitiatedByUser, ActivityDisplayName, TargetResources
| order by ActivityDateTime asc

The app is BudgetPlannerApp — named to blend with the phishing lure. Five permissions were granted: User.Read.All, Files.Read.All, Mail.Read, Notes.Read.All, and Files.ReadWrite.All. That scope gives the attacker full read access to the mailbox, all OneDrive files, all notes, and write access to files — everything needed to operate as the compromised user via Microsoft Graph API without ever needing their password again.
With OAuth access to marcus.reid’s mailbox, the attacker immediately sent internal phishing emails to three additional targets. The same Email sent query shows marcus.reid@nexgenenergy.com as sender at 02:10, 02:16, and 02:26 AM — all originating from 51.89.156.153, all with subject “Action Required: Verify Budget Planning App Access.”

The attacker is spreading the consent grant attack inward, using a trusted internal address to increase the chance of further victims clicking through.
At 02:25 AM, marcus.reid uploaded Immediate_Review.doc to the Finance SharePoint site. The filename is another urgency lure — likely used to deliver the OAuth phishing link to additional internal targets via a trusted document repository.
OfficeActivity_CL
| where OperationName == "FileUploaded"
| where UserId == "marcus.reid@nexgenenergy.com"
| take 1

The more valuable discovery came from enumerating marcus.reid’s OneDrive. The attacker accessed multiple files, and one stood out:
OfficeActivity_CL
| where OperationName == "FileAccessed"
| where UserId == "marcus.reid@nexgenenergy.com"
| order by ActivityDateTime asc

Provisioning-Script.ps1 — sitting in the user’s personal OneDrive documents folder at /personal/marcus_reid_nexgenenergy_com/Documents/. A PowerShell provisioning script with hardcoded service account credentials. This is one of the most common credential exposure patterns in cloud environments: a script written for convenience, never intended to be shared, sitting in a personal cloud drive that an OAuth-compromised account can read silently.
The credentials from Provisioning-Script.ps1 belonged to svc-provisioner@nexgenenergy.com. The attacker authenticated with this account from the same IP and began Azure resource group enumeration:
SigninLogs_CL
| where UserPrincipalName == "marcus.reid@nexgenenergy.com"

AzureActivity_CL
| where OperationName has "resourceGroups"
| where Caller has "svc-provisioner"
| summarize count() by ActivityStatus, ResourceGroup
| order by ActivityStatus asc

Two resource groups read successfully — RG-WebServices and RG-SharedOps. Two blocked — RG-FinanceCore and RG-Networking. The failures weren’t generic 403s though. Filtering on the ActivitySubStatus field revealed that RG-FinanceCore and its Key Vault kv-financecore returned Forbidden - ABAC condition not met:
AzureActivity_CL
| where ActivitySubStatus has "ABAC"

ABAC (Attribute-Based Access Control) in Azure allows access conditions to be tied to resource attributes — in this case, a resource tag. The attacker couldn’t read kv-financecore yet, but now knew the target existed.
Within RG-SharedOps — one of the accessible resource groups — the attacker found the AutomateOps Automation Account and read its runbooks:
AzureActivity_CL
| where OperationName has "automationAccounts"

Runbooks frequently contain hardcoded credentials for the service accounts they provision. The attacker read the Provision-Resources runbook output and extracted credentials for a second service account: svc-automation@nexgenenergy.com. This is visible in SigninLogs_CL — the new account authenticating from 51.89.156.153 shortly after:
SigninLogs_CL
| where IPAddress == "51.89.156.153"
| where UserPrincipalName has "svc"

With svc-provisioner still active, the attacker added it to the Finance-Operations group at 03:48 AM:
AuditLogs_CL
| where ActivityDisplayName has "Add member to group"
| where TargetResources has "svc-provisioner"

This group membership was the key that unlocked the ABAC condition on kv-financecore. The ABAC policy was conditioned on a resource tag — specifically CostCenter = FIN001. The attacker wrote this tag directly to the Key Vault resource:
AzureActivity_CL
| where Caller has "svc-provisioner"

Once the tag was applied, the ABAC condition was satisfied and kv-financecore became readable. This is a subtle but critical attack path — Azure ABAC policies that rely on resource tags are only as secure as the permissions around who can write tags. If a service account can modify tags, it can potentially satisfy its own access conditions.
With ABAC bypassed, svc-provisioner retrieved the secret jessica-turner-cred from kv-financecore:
AzureDiagnostics_CL
| where OperationName == "SecretGet"
| where CallerIpAddress == "51.89.156.153"

The operation name recorded in AzureDiagnostics for a successful secret retrieval is SecretGet — useful to know for detection rules.
Before moving to the high-privilege account, svc-automation added a new client secret to BudgetPlannerApp with Key ID 0309ccc7-c9ea-4b95-a8ee-e886b6c25422:
AuditLogs_CL
| where InitiatedByUser has "svc-automation"

This ensures that even if the original OAuth consent grant is discovered and revoked, the attacker retains a working credential for the application and can re-authenticate independently.
Using jessica-turner-cred, the attacker authenticated as jessica.turner@nexgenenergy.com — confirmed in SigninLogs_CL:
SigninLogs_CL
| where UserPrincipalName has "jessica"
| where IPAddress == "51.89.156.153"

Jessica Turner is a high-privilege account. The attacker used it to create a Temporary Access Pass for david.chen@nexgenenergy.com at 04:56 AM:
AuditLogs_CL
| where ActivityDisplayName has_any ("Create temporaryAccessPass", "Temporary access pass")

A TAP is a time-limited, PIN-based credential that bypasses normal authentication requirements including MFA. The default maximum validity period is 8 hours. By creating a TAP for david.chen, the attacker gained a clean authentication path to a second high-value account without needing to know or crack their password.
The attacker authenticated as david.chen@nexgenenergy.com from a second IP, 176.31.90.129, indicating a separate egress point for the final exfiltration phase:
SigninLogs_CL
| where UserPrincipalName has "david.chen"

Seven files were accessed from the Finance SharePoint site, starting with Investor-Presentation-2026.pptx:
OfficeActivity_CL
| where UserId == "david.chen@nexgenenergy.com"
| where OperationName == "FileAccessed"

All seven files were from the Finance site — confidential financial documents. The attacker came in through a phishing email and walked out with investor-grade financial data without triggering a single password reset or MFA challenge.
| Phase | Action |
|---|---|
| Initial Access | Typosquat phishing email from david@nexgenenrgy.com to marcus.reid |
| Credential Access | Illicit OAuth consent grant — BudgetPlannerApp granted 5 permissions |
| Lateral Phishing | 3 internal phishing emails sent from compromised marcus.reid account |
| Collection | Immediate_Review.doc uploaded to Finance SharePoint |
| Credential Access | Provisioning-Script.ps1 found in OneDrive — svc-provisioner credentials extracted |
| Lateral Movement | svc-provisioner authenticated from 51.89.156.153 |
| Discovery | Azure resource group enumeration — 2 read, 2 blocked by ABAC |
| Credential Access | AutomateOps runbook read — svc-automation credentials extracted |
| Privilege Escalation | svc-provisioner added to Finance-Operations group |
| Defense Evasion | CostCenter=FIN001 tag written to kv-financecore to satisfy ABAC condition |
| Credential Access | jessica-turner-cred retrieved from kv-financecore via SecretGet |
| Persistence | Client secret 0309ccc7 added to BudgetPlannerApp by svc-automation |
| Lateral Movement | jessica.turner authenticated — high-privilege account compromised |
| Persistence | TAP created for david.chen@nexgenenergy.com |
| Exfiltration | 7 Finance files accessed from david.chen account via 176.31.90.129 |
| Type | Value |
|---|---|
| IP (Primary Attacker) | 51[.]89[.]156[.]153 |
| IP (Final Stage) | 176[.]31[.]90[.]129 |
| Email (Attacker) | david@nexgenenrgy[.]com |
| App | BudgetPlannerApp |
| App Client Secret Key ID | 0309ccc7-c9ea-4b95-a8ee-e886b6c25422 |
| Account (Compromised) | marcus.reid@nexgenenergy[.]com |
| Account (Service) | svc-provisioner@nexgenenergy[.]com |
| Account (Service) | svc-automation@nexgenenergy[.]com |
| Account (High Privilege) | jessica.turner@nexgenenergy[.]com |
| Account (Final Target) | david.chen@nexgenenergy[.]com |
| File | Provisioning-Script.ps1 |
| File | Immediate_Review.doc |
| File | Investor-Presentation-2026.pptx |
| Key Vault | kv-financecore |
| Secret | jessica-turner-cred |
| Tag (ABAC Bypass) | CostCenter = FIN001 |
| Threat Actor | Storm-0558 |
| Technique | ID | Description |
|---|---|---|
| Spearphishing Link | T1566.002 | Typosquat phishing email delivering OAuth consent link |
| Steal Application Access Token | T1528 | Illicit consent grant — BudgetPlannerApp OAuth token |
| Cloud Accounts | T1078.004 | Service account compromise via hardcoded credentials |
| Credentials in Files | T1552.001 | Provisioning-Script.ps1 with hardcoded service account creds |
| Cloud Service Dashboard | T1538 | Azure resource group and Automation Account enumeration |
| Additional Cloud Credentials | T1098.001 | Client secret added to BudgetPlannerApp for persistence |
| Modify Authentication Process | T1556 | Temporary Access Pass created for david.chen |
| Domain Policy Modification | T1484.002 | svc-provisioner added to Finance-Operations group |
| Data from Cloud Storage | T1530 | 7 Finance SharePoint files accessed via david.chen |
| Credentials from Password Stores | T1555 | jessica-turner-cred retrieved from Azure Key Vault |
Disable user consent for OAuth applications — the entire attack chain begins because a standard user could grant a third-party application access to their mailbox and files without admin approval. Setting the tenant-wide consent policy to “Do not allow user consent” forces all app consent through an admin review workflow, eliminating the illicit consent grant vector entirely.
Audit Automation Account runbooks for hardcoded credentials — AutomateOps contained a runbook that exposed svc-automation credentials in plaintext job output. Runbooks should authenticate exclusively via Managed Identity or credential assets with restricted access, never via hardcoded strings. Regular audits of runbook content and job output logs should be part of the cloud security baseline.
Restrict tag write permissions on sensitive resources — the ABAC bypass worked because svc-provisioner had sufficient permissions to write resource tags to kv-financecore. ABAC policies conditioned on tags are only effective if tag modification is tightly controlled. Restricting Microsoft.Resources/tags/write on sensitive resources to dedicated privileged roles closes this bypass path.
Monitor for Temporary Access Pass creation — TAP creation by a service account or outside of a standard IT provisioning workflow is a strong indicator of compromise. AuditLogs activity Create Temporary Access Pass should trigger an alert whenever the initiating account is not a known IT admin identity. The 8-hour default validity window gives an attacker significant dwell time if this goes undetected.
Treat OAuth consent grant events as high-priority alerts — Add delegated permission grant in AuditLogs is one of the most reliable signals for this attack class. Alerting on any non-admin user granting permissions to an unverified publisher application, especially with scopes like Mail.Read or Files.ReadWrite.All, would have surfaced this attack within minutes of the initial phishing click.