// Illustrative; not executed against a live workspace. Microsoft Sentinel / Log Analytics: // AADServicePrincipalSignInLogs and AuditLogs. The Storm-0940 pattern Microsoft described on 21 October 2026: // a third-party app signs in from a network it has not used in the previous 30 days, then the same service // principal adds credentials, assigns roles or grants consent. Review candidates, not verdicts: vendors move // hosting, so confirm with the vendor before you revoke anything, and preserve the logs first. let Window = 7d; let Baseline = 30d; let PublishedAppIds = dynamic(["506e08f9-6e9a-4da4-942b-a099a03644ae", "24205314-2b89-4e07-a2a4-579e898a5640", "0de14c77-b9b0-4137-9360-34878a929827"]); // ConnectWise app IDs in Microsoft's 21 October 2026 post let MicrosoftOwnerTenants = dynamic(["f8cdef31-a31e-4b4a-93e4-5f571e91255a", "72f988bf-86f1-41af-91ab-2d7cd011db47"]); let ChangeOperations = dynamic(["Add service principal credentials", "Add member to role", "Add eligible member to role", "Add scoped member to role", "Add app role assignment to service principal", "Add delegated permission grant", "Consent to application", "Add owner to application", "Add owner to service principal"]); let VendorSignIns = AADServicePrincipalSignInLogs | where TimeGenerated > ago(Window + Baseline) | where ResultType == "0" | where (AppOwnerTenantId != AADTenantId and not(AppOwnerTenantId in (MicrosoftOwnerTenants))) or AppId in (PublishedAppIds) | extend Network = iff(isempty(AutonomousSystemNumber), IPAddress, strcat("AS", AutonomousSystemNumber)); let Known = VendorSignIns | where TimeGenerated <= ago(Window) | summarize KnownNetworks = make_set(Network, 500) by AppId; let NewNetworkSignIns = VendorSignIns | where TimeGenerated > ago(Window) | join kind=leftouter Known on AppId | where isnull(KnownNetworks) or not(set_has_element(KnownNetworks, Network)) or AppId in (PublishedAppIds) | summarize FirstNewSignIn = min(TimeGenerated), LastNewSignIn = max(TimeGenerated), SignIns = count(), Networks = make_set(Network, 10), IPs = make_set(IPAddress, 20), UserAgents = make_set(UserAgent, 10), CredentialTypes = make_set(ClientCredentialType, 5), Resources = make_set(ResourceDisplayName, 10) by AppId, ServicePrincipalId, ServicePrincipalName; let ChangesByApps = AuditLogs | where TimeGenerated > ago(Window) | where OperationName in~ (ChangeOperations) or OperationName contains "Certificates and secrets management" | extend ActorServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId) | where isnotempty(ActorServicePrincipalId) | summarize FirstChange = min(TimeGenerated), Changes = make_set(OperationName, 20), ChangedTargets = make_set(tostring(TargetResources[0].displayName), 20) by ActorServicePrincipalId; NewNetworkSignIns | join kind=leftouter ChangesByApps on $left.ServicePrincipalId == $right.ActorServicePrincipalId | extend ChangedDirectoryAfterNewSignIn = isnotnull(FirstChange) and FirstChange >= FirstNewSignIn | project-away ActorServicePrincipalId | order by ChangedDirectoryAfterNewSignIn desc, FirstNewSignIn asc