In Part 1 we used Microsoft Graph change notifications to watch privileged group membership in near real time. Graph subscriptions are perfect for that, but they do not cover sign-in events. If you want to know the instant a break-glass account logs in, or when an admin account picks up a high risk score, you need a different pipe.

The production-correct pattern for that is to stream Entra ID’s SignInLogs through diagnostic settings to an Event Hub, then consume the hub from the same Azure Function App. In this post we wire that up end to end and add a filter that pages on duty when a break-glass UPN signs in or when sign-in risk crosses a threshold. Combined with Part 1 you end up with one Function App that watches both privileged group changes and privileged account use.

What we are building

Entra ID emits sign-in logs to its diagnostic settings sink within roughly two to five minutes of the event. We point that sink at an Event Hub, attach an Azure Function with an Event Hub trigger, and process each batch of records. The function filters on UPN and risk level, and reuses the Teams alert helper we already shipped in Part 1.

Prerequisites

  • The Function App and Teams Incoming Webhook from Part 1
  • An Entra ID P1 or P2 license for sign-in log streaming through diagnostic settings
  • Owner or Contributor on a resource group where you can deploy an Event Hubs namespace
  • Az.EventHub PowerShell module 5.x or newer
  • An account with Global Administrator or Security Administrator to configure tenant diagnostic settings

Step 1: Event Hubs namespace and hub

A single Basic tier namespace with one hub is more than enough for sign-in traffic in a normal tenant. The script below uses Standard so you get the 7 day retention and Capture if you want it later.

$rg       = 'rg-entra-sec'
$location = 'westeurope'
$nsName   = 'evhns-entra-sec-prod'
$hubName  = 'signinlogs'

# 1. Namespace (Standard tier, 1 throughput unit)
New-AzEventHubNamespace `
    -ResourceGroupName $rg -Name $nsName `
    -Location $location -SkuName Standard -SkuCapacity 1

# 2. Event hub, 2 partitions, 7 day retention (168 hours)
New-AzEventHub `
    -ResourceGroupName $rg -NamespaceName $nsName `
    -Name $hubName -PartitionCount 2 -RetentionTimeInHour 168

# 3. Namespace level authorization rules
New-AzEventHubAuthorizationRule `
    -ResourceGroupName $rg -NamespaceName $nsName `
    -Name 'fn-listen' -Rights Listen

New-AzEventHubAuthorizationRule `
    -ResourceGroupName $rg -NamespaceName $nsName `
    -Name 'fn-send'   -Rights Send

# 4. Grab the connection strings
$listen = Get-AzEventHubKey -ResourceGroupName $rg -NamespaceName $nsName -Name 'fn-listen'
$send   = Get-AzEventHubKey -ResourceGroupName $rg -NamespaceName $nsName -Name 'fn-send'

Write-Host "Listen conn: $($listen.PrimaryConnectionString)"
Write-Host "Send conn:   $($send.PrimaryConnectionString)"

Store both connection strings in Key Vault and reference them from app settings. The fn-listen rule feeds the Function App in Step 3, and fn-send is used by the diagnostic settings sink in the next step.

Step 2: Point Entra ID diagnostic settings at the hub

The diagnostic settings configuration lives at the tenant level, not on a subscription. You need Microsoft.Graph module 2.x and a sign in with Security Administrator or Global Administrator.

Connect-MgGraph -Scopes "AuditLog.Read.All","Directory.Read.All"

# Use the Azure CLI for the tenant-level diagnostic settings API,
# the Microsoft.Insights provider exposes it at the tenant scope.
$nsId  = "/subscriptions/<sub>/resourceGroups/rg-entra-sec/providers/Microsoft.EventHub/namespaces/evhns-entra-sec-prod"
$rule  = "$nsId/authorizationRules/fn-send"
$body  = @{
    properties = @{
        eventHubAuthorizationRuleId = $rule
        eventHubName                = "signinlogs"
        logs = @(
            @{ category = "SignInLogs";             enabled = $true }
            @{ category = "NonInteractiveUserSignInLogs"; enabled = $true }
            @{ category = "ServicePrincipalSignInLogs";   enabled = $true }
        )
    }
} | ConvertTo-Json -Depth 10

az rest --method put `
    --url "https://management.azure.com/providers/microsoft.aadiam/diagnosticSettings/entra-sec-stream?api-version=2017-04-01-preview" `
    --body $body

Once the diagnostic setting saves, sign-in records start flowing into the hub. Generate a real sign-in to verify the pipeline before you spend time on filtering logic.

Step 3: The Event Hub trigger

The function receives an array of records on each invocation. Each record is the JSON shape Entra ID writes to the audit pipeline, the schema is documented under “Tenant sign-in logs streaming schema.” The fields we care about are userPrincipalName, riskLevelDuringSignIn, riskLevelAggregated, status.errorCode, and the source IP.

# function.json binds: name "events", type "eventHubTrigger",
# connection "EVH_LISTEN", eventHubName "signinlogs", cardinality "many"
param([object[]] $events, $TriggerMetadata)

$breakGlass = @($env:BREAKGLASS_UPNS -split ',' | ForEach-Object { $_.Trim().ToLower() })
$riskTrigger = @('high','medium')

foreach ($e in $events) {
    $upn  = ($e.properties.userPrincipalName ?? '').ToLower()
    $risk = ($e.properties.riskLevelDuringSignIn ?? 'none').ToLower()
    $code = $e.properties.status.errorCode

    $isBreakGlass = $breakGlass -contains $upn
    $isHighRisk   = $riskTrigger -contains $risk

    if (-not ($isBreakGlass -or $isHighRisk)) { continue }

    $reason = if ($isBreakGlass) { "Break-glass account sign-in" } else { "Sign-in risk $risk" }
    Send-TeamsAlert `
        -Reason  $reason `
        -Upn     $upn `
        -AppName $e.properties.appDisplayName `
        -Ip      $e.properties.ipAddress `
        -Status  ($code -eq 0 ? "Success" : "Failed ($code)") `
        -When    $e.time
}

The ?? null coalescing operator needs PowerShell 7, which the Function App runs by default. The same Send-TeamsAlert helper from Part 1 handles the actual webhook POST. Keep the function simple, the Event Hub trigger has at-least-once delivery semantics, so any side effect you add needs to be idempotent.

Step 4: Tighten the filter

A few rules that are worth adding before this goes anywhere near a 24x7 rotation. Break-glass accounts should never sign in successfully outside a declared incident, so a single success is a genuine page. Failed break-glass attempts are also high signal because they indicate someone trying to guess or replay the credentials. For risk events, deduplicate on correlationId for ten minutes to avoid spamming the channel during sign-in retries.

function Test-ShouldAlert {
    param($Event, $RecentCorrelations)

    $upn  = ($Event.properties.userPrincipalName ?? '').ToLower()
    $risk = ($Event.properties.riskLevelDuringSignIn ?? 'none').ToLower()
    $cid  = $Event.properties.correlationId

    if ($script:BreakGlass -contains $upn) { return $true }

    if ($risk -in 'medium','high') {
        if ($RecentCorrelations.ContainsKey($cid)) { return $false }
        $RecentCorrelations[$cid] = (Get-Date).AddMinutes(10)
        return $true
    }

    return $false
}

Persist $RecentCorrelations in a Storage Table keyed by correlation ID with a TTL so dedup survives function instance recycles.

Putting it all together

End to end the flow for a break-glass sign-in looks like this. The account signs in. Entra ID writes the record to its audit pipeline within a couple of minutes. Diagnostic settings forwards the record to the Event Hub. The Function App fires within a second of the hub receiving the batch, filters, builds the Teams card, and posts it. Realistic latency from “credentials submitted” to “Teams card visible” is in the three to six minute range, dominated by the diagnostic settings flush interval, which is a tenant level setting you cannot tune.

Together with the group membership watcher from Part 1, the Function App now covers two of the highest signal Entra ID events. Three more upgrades worth doing once the pipeline is stable. Send the same alerts to your SIEM through a second Event Hub consumer group so the security team has the raw events alongside the Teams notification. Add a sign-in IP allowlist for break-glass UPNs and treat sign-ins from anywhere else as red, even on success. Finally, run a quarterly fire drill by triggering a controlled break-glass login and timing how long it takes the on-call to acknowledge, that is the single most useful number you will get out of this whole setup.