Introduction
In this blog, we demonstrate how to identify a user logging into an Azure-based application for the first time, then trigger an automated email alert to the Application Support Team.
The key to this solution is an Azure Alert that polls the Log Analytics workspace where application logins are captured.
SigninLog
| where AppId == ‘xxxx-xxxx-xxxx-xxxx-xxxx’
| where TimeGenerated > ago(5m)
If at least one sign-in has occurred in the last five minutes an Azure Runbook is called.
The runbook retrieves the application sign-in events from the Log Analytic workspace and summarises by user.
If a ‘first-time’ user is found an email notification is sent to the Application Support Team.
Example email notification:
From: Company <noreply@company.com.au>
Sent: Friday, 8 December 2023 9:55 AM
To: WW Business Projects <wwbusinessprojects@company.com.au>
Subject: New Process Manager application user – JONES Kelly
Dear Support,
A new user has signed into Process Manager for the first time, please complete onboarding activities.
Username: JONES Kelly
Email: KJONES@company.qld.gov.au
Directorate: Business Services
Branch: Chief Information Office
Department: Enterprise Platforms
The business unit will contact the user as required.
Kind regards,
City Process Manager Team
Alert Rule Configuration
Log Search Alert Rule
"id": "/subscriptions/xxxxxxx-xxxx-xxxx-xxxx-xxxxxxx/resourceGroups/rg-p-ase-securitycontrols/providers/microsoft.insights/scheduledqueryrules/ProMapp",
"name": "ProMapp",
"type": "Microsoft.Insights/scheduledQueryRules",
"location": "australiasoutheast",
"tags": {
"Business Owner": "Jim Smith",
"Environment": "Production",
"ICTSystem": "Azure Platform",
"Security": "HighBIL"
},
"systemData": {
"createdBy": "PSMITH@company.com.au",
"createdByType": "User",
"createdAt": "2023-11-21T21:48:20.4180184Z",
"lastModifiedBy": "PSMITH@company.com.au",
"lastModifiedByType": "User",
"lastModifiedAt": "2023-11-26T22:06:17.7386834Z"
},
"properties": {
"createdWithApiVersion": "2023-03-15-preview",
"displayName": "ProMapp",
"description": "",
"severity": 3,
"enabled": true,
"evaluationFrequency": "PT5M",
"scopes": [
"/subscriptions/xxxxxxx-xxxx-xxxx-xxxx-xxxxxxx/resourceGroups/rg-p-ase-securitycontrols/providers/Microsoft.OperationalInsights/workspaces/la-p-ase-security"
],
"targetResourceTypes": [
"Microsoft.OperationalInsights/workspaces"
],
"windowSize": "PT5M",
"overrideQueryTimeRange": "P2D",
"criteria": {
"allOf": [
{
"query": "SigninLogs\n| where AppId == 'xxxxxxxx-a25a-4615-ac10-xxxxxxxxxxx'\n| where TimeGenerated > ago(5m)\n",
"timeAggregation": "Count",
"dimensions": [],
"operator": "GreaterThanOrEqual",
"threshold": 1,
"failingPeriods": {
"numberOfEvaluationPeriods": 1,
"minFailingPeriodsToAlert": 1
}
}
]
},
"autoMitigate": false,
"actions": {
"actionGroups": [
"/subscriptions/xxxxxxx-xxxx-xxxx-xxxx-xxxxxxx/resourceGroups/rg-p-ase-securitycontrols/providers/microsoft.insights/actionGroups/ProMapp"
],
"customProperties": {}
}
}
}
PowerShell Automation Runbook
#----------------------------- # INITIALISE #----------------------------- Import-Module Az.OperationalInsights #----------------------------- # FUNCTIONS #----------------------------- function GetAccessTokenHeaders($tenantId, $appId, $appSecret){ $uri = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" $body = @{ client_id = $appId scope = "https://graph.microsoft.com/.default" client_secret = $appSecret grant_type = "client_credentials" } try{ $tokenRequest = Invoke-WebRequest -Method Post -Uri $uri -ContentType "application/x-www-form-urlencoded" -Body $body -UseBasicParsing # Unpack Access Token $token = ($tokenRequest.Content | ConvertFrom-Json).access_token $headers = @{ 'Content-Type' = "application\json" 'Authorization' = "Bearer $Token" } } catch { $headers = $null } return $headers } # create the email message parameters - 26/04/22 function GetMessageParams($emailFrom, $emailTo, $htmlMsg, $headers, $userName){ $recipientList = [System.Collections.ArrayList]@() $null = $recipientList.Add(@{emailAddress = @{address = ($emailTo)}}) $bccRecipientList = [System.Collections.ArrayList]@() $null = $bccRecipientList.Add(@{emailAddress = @{address = ('psmith@company.com.au')}}) return @{ URI = "https://graph.microsoft.com/v1.0/users/$emailFrom/sendMail" Headers = $headers Method = "POST" ContentType = 'application/json' Body = (@{ message = @{ subject = "New Process Manager user - $userName" attachments = @( @{ "@odata.type"= "#microsoft.graph.fileAttachment" name = 'company_Logo' contentType = 'image/gif' contentBytes = $companyLogo } ) body = @{ contentType = 'HTML' content = $htmlMsg } toRecipients = $recipientList bccRecipients = $bccRecipientList } } ) | ConvertTo-JSON -Depth 6 } } # Build the email bogy function BuildMessage($userDetails){ return("<-html> <-style> {font-family: Arial; font-size: 13pt;} TABLE{border: 1px solid black; border-collapse: collapse; font-size:13pt;} TH{border: 1px solid black; background: #dddddd; padding: 5px; color: #000000;} TD{border: 1px solid black; padding: 10px; Text-align:Center } <-/style> <-h3>Dear Support,<-/h3> A new user has signed into Process Manager for the first time, please complete onboarding activities. <-br> <-br>User name: <-b>$($userDetails.Name)<-/b> <-br>Payroll: <-b>$($userDetails.Payroll)<-/b> <-br>Email: $($userDetails.Email) <-br>Directorate: <-b>$($userDetails.Directorate)<-/b> <-br>Branch: <-b>$($userDetails.Branch)<-/b> <-br>Department: <-b>$($userDetails.Department)<-/b> <-br><-br> The business unit will contact the user as required. <-br><-br> Kind regards, <-br> City Process Manager Team <-br> <-br> <-a href='https://www.company.com.au/default.html'> <-img border=0 src='cid:CoGC_Logo'><-/a> <-/body><-/html>") } #----------------------------- # MAIN #----------------------------- try{ $errorMessage = 'unable to login using AA managed identity' Connect-AzAccount -Identity Set-AzContext 'Prod' $appId = $null # retrieved from Key Vault $appSecret = $null # retrieved from Key Vault $tenantId = "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxx" # Azure tenant $recipientEmail = 'wwbusinessprojects@company.com.au' Write-Output "recipientEmail: $recipientEmail" # Get the log analytics workspace $errorMessage = 'OperationalInsightsQuery failed' $workspace = Get-AzOperationalInsightsWorkspace -ResourceGroupName "rg-p-ase-securitycontrols" -Name "la-p-ase-security" # get all past signins for ProMapp $query = "SigninLogs | where AppId == 'xxxxxxx-xxxx-xxxx-xxxx-xxxxxxx'" $result = Invoke-AzOperationalInsightsQuery -Workspace $workspace -Query $query # sumarise user sign-ins using a hash $userSignIns = @{} $count = 0 foreach($signIn in $result.Results){ $userKey = "$($signIn.UserDisplayName.Split(' ')[-1])~$($signIn.UserPrincipalName)" if($userSignIns.Contains($userKey)){ $data = $userSignIns.Get_Item($userKey) $data += $signIn.TimeGenerated $userSignIns.Set_Item($userKey,$data) } else { $userSignIns.Add($userKey,@($signIn.TimeGenerated)) } $count ++ } <# testing $userSignIns $userSignIns.Count #> Write-Output "$count application signins" Write-Output "$($userSignIns.Count) unique users" # check for sign-ins within last 5 minutes - only check users with one sign-in foreach($user in $userSignIns.GetEnumerator() | Where-Object{$_.Value.Count -eq 1}){ $time = $user.value[0] $timeDiff = New-TimeSpan –Start $(Get-Date) –End $time $newUserCount = 0 if($timeDiff.TotalMinutes -ge -5){ Write-Output "Processing new user: $($user.Name), $time " $newUserCount ++ if(-not $appId){ # initialisation for first user $errorMessage = 'unable to access Key Vault' $appId = Get-AzKeyVaultSecret -VaultName kv-p-ae-cloudteam -Name AzureAutomationAppId -AsPlainText # only valid for PS 5.1 $appSecret = Get-AzKeyVaultSecret -VaultName kv-p-ae-cloudteam -Name AzureAutomationAppSecret -AsPlainText # only valid for PS 5.1 # download the company logo file from Azure storage $errorMessage = 'unable to download logo file from storage' $sa = Get-AzStorageAccount -ResourceGroupName rg-p-ae-cloudteam -Name companyaecloudteam $null = Get-AzStorageBlobContent -Blob 'company_logo.png' -Container 'config' -Context $($sa.Context) -Destination "$env:TEMP\cogc_logo.png" -Force $cogcLogo = [convert]::ToBase64String((Get-Content "$env:TEMP\company_logo.png" -Encoding Byte -ReadCount 0)) Write-Output 'Downloaded company logo file' $headers = GetAccessTokenHeaders $tenantId $appId $appSecret if(-not $headers){ $errorMessage = 'GetAccessTokenHeaders failed' throw $null } } # login was within last 5 minutes # get user's details $userEmail = $user.Name.Split('~')[-1] $errorMessage = 'get user details failed' #$resourcesUrl = "https://graph.microsoft.com/v1.0/groups/xxxxxxx-250e-417f-8694-xxxxxxxxxxxx/members/microsoft.graph.user?`$select=employeeId,&`$expand=manager(`$select=displayName,userPrincipalName,employeeId,extension_eccb56601fe44c7584b1a2134dd30bc9_cogcdirectoratedesc,extension_eccb56601fe44c7584b1a2134dd30bc9_cogcbranchdesc,extension_eccb56601fe44c7584b1a2134dd30bc9_cogcsectiondesc,jobTitle)" $resourcesUrl = "https://graph.microsoft.com/v1.0/users/$($userEmail)?`$select=displayName,userPrincipalName,employeeId,officeLocation,extension_eccb56601fe44c7584b1a2134dd30bc9_cogcdirectoratedesc,extension_eccb56601fe44c7584b1a2134dd30bc9_cogcbranchdesc,extension_eccb56601fe44c7584b1a2134dd30bc9_cogcsectiondesc,extension_eccb56601fe44c7584b1a2134dd30bc9_cogccostcentredesc,jobTitle&`$expand=manager" $response = Invoke-RestMethod -Method Get -Uri $resourcesUrl -Headers $headers -ContentType 'application/json' $userDetails = [PsCustomObject]@{ Name = $response.displayName Payroll = $response.employeeId Email = $response.userPrincipalName Directorate = $response.extension_eccb56601fe44c7584b1a2134dd30bc9_cogcdirectoratedesc Branch = $response.extension_eccb56601fe44c7584b1a2134dd30bc9_cogcbranchdesc Department = $response.extension_eccb56601fe44c7584b1a2134dd30bc9_cogcsectiondesc } Write-Output ". Payroll: $($userDetails.Payroll)" Write-Output ". Email: $($userDetails.Email)" Write-Output ". Directorate: $($userDetails.Directorate)" Write-Output ". Branch: $($userDetails.Branch)" Write-Output ". Department: $($userDetails.Department)" # send email to user $htmlMsg = BuildMessage $userDetails $MessageParams = GetMessageParams -emailFrom 'noreply@company.com.au' -emailTo $recipientEmail $htmlMsg $headers -userName $userDetails.Name $errorMessage = 'unable to send email' Invoke-RestMethod @Messageparams <# testing $MessageParams.Body.message | fl #> Write-Output 'Email notification sent' } } } catch { Write-Output "ERROR: $errorMessage" Write-Output ". system message: $($_.Exception.Message)" } <# testing foreach($signIn in $result.Results){ "$($signIn.UserDisplayName.Split(' ')[-1])~$($signIn.UserPrincipalName)" } #>


0 Comments