Sync Azure Active Directory Risk Events with a SharePoint list for all customer tenants
According to Microsoft, the vast majority of security breaches occur when attackers gain access to data using a compromised identity. To help combat this, Azure Active Directory uses machine learning and advanced heuristics to identify suspicious activities and generate risk events.
These risk events are generated for:
- Users with leaked credentials
- Users signing in from anonymous IP addresses
- Impossible travel events
- Sign-ins from infected devices
- Sign-ins from suspicious IP Addresses
- Sign-ins from unfamiliar locations
Right now these events are only available in Azure AD premium plans. For more information on them, see here: https://docs.microsoft.com/en-us/azure/active-directory/reports-monitoring/concept-risk-events
If you are managing a single tenant and have Azure AD Premium P1 or P2, risk events can be easily accessed via the Azure AD Admin portal here.
If you are a Microsoft Partner and need visibility across risk events for all customer tenants, you can access them using the Microsoft Graph API. This article will show you how to sync these risk events with a SharePoint list, and set up alerts to your email or service desk.
This guide consists of three sections:
- Create an Azure AD App with Access to your customer tenants’ risk events
- Sync the Azure AD risk events with a SharePoint List using PowerShell
- Add an alert to the SharePoint list to get notified of new risk events
Create an Azure AD application with access to your customer’s Azure AD risk Events
The below script will create an application in your Office 365 organization that has permission to access your customer tenants’ risk events.
Prerequisites
- This script will need to be run as a Global Administrator in your tenant
- This script requires the Azure AD Module. You can install this by opening PowerShell as an Administrator and running:
Install-Module AzureAD
- If you are running this in a non-Microsoft Partner tenant, remove the following three lines from the script.
# This provides the application with access to your customer tenants. $group = Get-AzureADGroup -Filter "displayName eq 'Adminagents'" Add-AzureADGroupMember -ObjectId $group.ObjectId -RefObjectId $servicePrincipal.ObjectId
How to create an Azure AD application with access to customer Azure AD risk events via PowerShell
- Double click the below script to select it
- Copy and paste it into Visual Studio Code
- Save it with a .ps1 extension
- Install the recommended PowerShell extension if you haven’t already
- Update the $homepage and $appIdURI variables to use a verified domain in your own tenant.
- Press F5 to run it, sign in with your Global Admin Credentials and wait for it to complete. (Note that the output may vary slightly from this screenshot)
- Retrieve the Application’s Client ID and secret from the CSV located at C:\temp\AzureADApps.csv. Remember to delete the CSV when complete. Note that this screenshot is just a sample.
PowerShell script to create an Azure AD application with access to Microsoft Graph
# This script needs to be run by an admin account in your Office 365 tenant # This script will create an Azure AD app in your organisation with permission # to access resources in yours and your customers' tenants. # It will export information about the application to a CSV located at C:\temp\. # The CSV will include the Client ID and Secret of the application, so keep it safe. # Confirm C:\temp exists $temp = Test-Path -Path C:\temp if ($temp) { #Write-Host "Path exists" } else { Write-Host "Creating Temp folder" New-Item -Path C:\temp -ItemType directory } $applicationName = "GCITS Risk Event Manager" # Change this to true if you would like to overwrite any existing applications with matching names. $removeExistingAppWithSameName = $false # Modify the homePage, appIdURI and logoutURI values to whatever valid URI you like. # They don't need to be actual addresses, so feel free to make something up. $homePage = "https://secure.gcits.com" $appIdURI = "https://secure.gcits.com/$((New-Guid).ToString())" $logoutURI = "https://portal.office.com" $URIForApplicationPermissionCall = "https://graph.microsoft.com/beta/identityRiskEvents" $ApplicationPermissions = "IdentityRiskEvent.Read.All IdentityriskyUser.Read.All Directory.Read.All Sites.Manage.All" Function Add-ResourcePermission($requiredAccess, $exposedPermissions, $requiredAccesses, $permissionType) { foreach ($permission in $requiredAccesses.Trim().Split(" ")) { $reqPermission = $null $reqPermission = $exposedPermissions | Where-Object {$_.Value -contains $permission} Write-Host "Collected information for $($reqPermission.Value) of type $permissionType" -ForegroundColor Green $resourceAccess = New-Object Microsoft.Open.AzureAD.Model.ResourceAccess $resourceAccess.Type = $permissionType $resourceAccess.Id = $reqPermission.Id $requiredAccess.ResourceAccess.Add($resourceAccess) } } Function Get-RequiredPermissions($requiredDelegatedPermissions, $requiredApplicationPermissions, $reqsp) { $sp = $reqsp $appid = $sp.AppId $requiredAccess = New-Object Microsoft.Open.AzureAD.Model.RequiredResourceAccess $requiredAccess.ResourceAppId = $appid $requiredAccess.ResourceAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.ResourceAccess] if ($requiredDelegatedPermissions) { Add-ResourcePermission $requiredAccess -exposedPermissions $sp.Oauth2Permissions -requiredAccesses $requiredDelegatedPermissions -permissionType "Scope" } if ($requiredApplicationPermissions) { Add-ResourcePermission $requiredAccess -exposedPermissions $sp.AppRoles -requiredAccesses $requiredApplicationPermissions -permissionType "Role" } return $requiredAccess } Function New-AppKey ($fromDate, $durationInYears, $pw) { $endDate = $fromDate.AddYears($durationInYears) $keyId = (New-Guid).ToString() $key = New-Object Microsoft.Open.AzureAD.Model.PasswordCredential($null, $endDate, $keyId, $fromDate, $pw) return $key } Function Test-AppKey($fromDate, $durationInYears, $pw) { $testKey = New-AppKey -fromDate $fromDate -durationInYears $durationInYears -pw $pw while ($testKey.Value -match "\+" -or $testKey.Value -match "/") { Write-Host "Secret contains + or / and may not authenticate correctly. Regenerating..." -ForegroundColor Yellow $pw = Initialize-AppKey $testKey = New-AppKey -fromDate $fromDate -durationInYears $durationInYears -pw $pw } Write-Host "Secret doesn't contain + or /. Continuing..." -ForegroundColor Green $key = $testKey return $key } Function Initialize-AppKey { $aesManaged = New-Object "System.Security.Cryptography.AesManaged" $aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC $aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::Zeros $aesManaged.BlockSize = 128 $aesManaged.KeySize = 256 $aesManaged.GenerateKey() return [System.Convert]::ToBase64String($aesManaged.Key) } function Confirm-MicrosoftGraphServicePrincipal { $graphsp = Get-AzureADServicePrincipal -All $true | Where-Object {$_.displayname -eq "Microsoft Graph"} if (!$graphsp) { $graphsp = Get-AzureADServicePrincipal -SearchString "Microsoft.Azure.AgregatorService" } if (!$graphsp) { Login-AzureRmAccount -Credential $credentials New-AzureRmADServicePrincipal -ApplicationId "00000003-0000-0000-c000-000000000000" $$graphsp = Get-AzureADServicePrincipal -All $true | Where-Object {$_.displayname -eq "Microsoft Graph"} } return $graphsp } Write-Host "Connecting to Azure AD. The login window may appear behind Visual Studio Code." Connect-AzureAD Write-Host "Creating application in tenant: $((Get-AzureADTenantDetail).displayName)" # Check for the Microsoft Graph Service Principal. If it doesn't exist already, create it. $graphsp = Confirm-MicrosoftGraphServicePrincipal $existingapp = $null $existingapp = get-azureadapplication -SearchString $applicationName if ($existingapp -and $removeExistingAppWithSameName) { Remove-Azureadapplication -ObjectId $existingApp.objectId } # RSPS $rsps = @() if ($graphsp) { $rsps += $graphsp $tenant_id = (Get-AzureADTenantDetail).ObjectId $tenantName = (Get-AzureADTenantDetail).DisplayName # Add Required Resources Access (Microsoft Graph) $requiredResourcesAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.RequiredResourceAccess] $microsoftGraphRequiredPermissions = Get-RequiredPermissions -reqsp $graphsp -requiredApplicationPermissions $ApplicationPermissions -requiredDelegatedPermissions $DelegatedPermissions $requiredResourcesAccess.Add($microsoftGraphRequiredPermissions) # Get an application key $pw = Initialize-AppKey $fromDate = [System.DateTime]::Now $appKey = Test-AppKey -fromDate $fromDate -durationInYears 99 -pw $pw Write-Host "Creating the AAD application $applicationName" -ForegroundColor Blue $aadApplication = New-AzureADApplication -DisplayName $applicationName ` -HomePage $homePage ` -ReplyUrls $homePage ` -IdentifierUris $appIdURI ` -LogoutUrl $logoutURI ` -RequiredResourceAccess $requiredResourcesAccess ` -PasswordCredentials $appKey ` -AvailableToOtherTenants $true # Creating the Service Principal for the application $servicePrincipal = New-AzureADServicePrincipal -AppId $aadApplication.AppId Write-Host "Assigning Permissions" -ForegroundColor Yellow # Assign application permissions to the application foreach ($app in $requiredResourcesAccess) { $reqAppSP = $rsps | Where-Object {$_.appid -contains $app.ResourceAppId} Write-Host "Assigning Application permissions for $($reqAppSP.displayName)" -ForegroundColor DarkYellow foreach ($resource in $app.ResourceAccess) { if ($resource.Type -match "Role") { New-AzureADServiceAppRoleAssignment -ObjectId $serviceprincipal.ObjectId ` -PrincipalId $serviceprincipal.ObjectId -ResourceId $reqAppSP.ObjectId -Id $resource.Id } } } # This provides the application with access to your customer tenants. $group = Get-AzureADGroup -Filter "displayName eq 'Adminagents'" Add-AzureADGroupMember -ObjectId $group.ObjectId -RefObjectId $servicePrincipal.ObjectId Write-Host "App Created" -ForegroundColor Green # Define parameters for Microsoft Graph access token retrieval $client_id = $aadApplication.AppId; $client_secret = $appkey.Value $tenant_id = (Get-AzureADTenantDetail).ObjectId $resource = "https://graph.microsoft.com" $authority = "https://login.microsoftonline.com/$tenant_id" $tokenEndpointUri = "$authority/oauth2/token" # Get the access token using grant type password for Delegated Permissions or grant type client_credentials for Application Permissions $content = "grant_type=client_credentials&client_id=$client_id&client_secret=$client_secret&resource=$resource" # Try to execute the API call 6 times $Stoploop = $false [int]$Retrycount = "0" do { try { $response = Invoke-RestMethod -Uri $tokenEndpointUri -Body $content -Method Post -UseBasicParsing Write-Host "Retrieved Access Token" -ForegroundColor Green # Assign access token $access_token = $response.access_token $body = $null $body = Invoke-RestMethod ` -Uri $UriForApplicationPermissionCall ` -Headers @{"Authorization" = "Bearer $access_token"} ` -ContentType "application/json" ` -Method GET ` Write-Host "Retrieved Graph content" -ForegroundColor Green $Stoploop = $true } catch { if ($Retrycount -gt 5) { Write-Host "Could not get Graph content after 6 retries." -ForegroundColor Red $Stoploop = $true } else { Write-Host "Could not get Graph content. Retrying in 5 seconds..." -ForegroundColor DarkYellow Start-Sleep -Seconds 5 $Retrycount ++ } } } While ($Stoploop -eq $false) $appInfo = [pscustomobject][ordered]@{ ApplicationName = $ApplicationName TenantName = $tenantName TenantId = $tenant_id clientId = $client_id clientSecret = $client_secret ApplicationPermissions = $ApplicationPermissions } $AppInfo | Export-Csv C:\temp\AzureADApp.csv -Append -NoTypeInformation } else { Write-Host "Microsoft Graph Service Principal could not be found or created" -ForegroundColor Red }
Sync customer Azure AD risk events with a SharePoint List using PowerShell
Once you have created an application with access to your own, and your customers’, Azure AD Risk events, you can set up a regular sync of the risk events into a SharePoint list.
How to sync customer Azure AD risk events with a SharePoint List
- Double click the below script to select it
- Copy and paste it into Visual Studio Code
- Save it with a .ps1 extension
- Paste the Client ID and Secret generated by the previous script into the $ClientID and $Secret variables.
- Press F5 to run it and wait for it to complete
- You should now have a SharePoint list in your root SharePoint team site called Risk Event Register containing any risk events from your customer tenants. You can find this list under Site Contents.
- Once you are happy with the script you can set it up to run on a schedule using the tool of your choice – such as Azure Runbooks, Timer Triggered Azure Functions, Scheduled Tasks etc.
Script to sync customers Azure AD risk events with a SharePoint list using PowerShell and the Microsoft Graph
$appId = "ENTERCLIENTIDHERE" $secret = "ENTERCLIENTSECRETHERE" $ourTenantId = "ENTERYOURTENANTIDHERE" $tenantName = "ENTERYOURCOMPANYNAMEHERE" $tenantDomain = "ENTERANYOFYOURDOMAINSHERE" $ListName = "Risk Event Register" $siteid = "root" $graphBaseUri = "https://graph.microsoft.com/v1.0" function Get-GCITSAccessToken($appCredential, $tenantId) { $client_id = $appCredential.appID $client_secret = $appCredential.secret $tenant_id = $tenantid $resource = "https://graph.microsoft.com" $authority = "https://login.microsoftonline.com/$tenant_id" $tokenEndpointUri = "$authority/oauth2/token" $content = "grant_type=client_credentials&client_id=$client_id&client_secret=$client_secret&resource=$resource" $response = Invoke-RestMethod -Uri $tokenEndpointUri -Body $content -Method Post -UseBasicParsing $access_token = $response.access_token return $access_token } function Get-GCITSMSGraphResource($Resource) { $graphBaseUri = "https://graph.microsoft.com/beta" $values = @() $result = Invoke-RestMethod -Uri "$graphBaseUri/$resource" -Headers $headers if ($result.value) { $values += $result.value if ($result."@odata.nextLink") { do { $result = Invoke-RestMethod -Uri $result."@odata.nextLink" -Headers $headers $values += $result.value } while ($result."@odata.nextLink") } } else { $values = $result } return $values } function New-GCITSSharePointColumn($Name, $Type, $Indexed, $lookupListName, $lookupColumnPrimaryName, $lookupColumnName, $longText) { if ($longText) { $column = [ordered]@{ name = $Name indexed = $Indexed $Type = @{ maxLength = 0 allowMultipleLines = $True #appendChangesToExistingText = $False #linesForEditing = 6 #textType = "plain" } } } else { $column = [ordered]@{ name = $Name indexed = $Indexed $Type = @{ } } if ($lookupListName -and $type -contains "lookup") { $list = Get-GCITSSharePointList -ListName $lookupListName if ($list) { $column.lookup.listId = $list.id $column.lookup.columnName = $lookupColumnName } } } return $column } function New-GCITSSharePointList ($Name, $ColumnCollection) { $list = @{ displayName = $Name columns = $columnCollection } | Convertto-json -Depth 10 $newList = Invoke-RestMethod ` -Uri "$graphBaseUri/sites/$siteid/lists/" ` -Headers $SPHeaders ` -ContentType "application/json" ` -Method POST -Body $list return $newList } function Remove-GCITSSharePointList ($ListId) { $removeList = Invoke-RestMethod ` -Uri "$graphBaseUri/sites/$siteid/lists/$ListId" ` -Headers $SPHeaders ` -ContentType "application/json" ` -Method DELETE return $removeList } function Remove-GCITSSharePointListItem ($ListId, $ItemId) { $removeItem = Invoke-RestMethod ` -Uri "$graphBaseUri/sites/$siteid/lists/$ListId/items/$ItemId" ` -Headers $SPHeaders ` -ContentType "application/json" ` -Method DELETE return $removeItem } function New-GCITSSharePointListItem($ItemObject, $ListId) { $itemBody = @{ fields = $ItemObject } | ConvertTo-Json -Depth 10 $listItem = Invoke-RestMethod ` -Uri "$graphBaseUri/sites/$siteid/lists/$listId/items" ` -Headers $SPHeaders ` -ContentType "application/json" ` -Method Post ` -Body $itemBody } function Get-GCITSSharePointListItem($ListId, $ItemId, $Query) { if ($ItemId) { $listItem = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items/$ItemId ` -Method Get -headers $SPHeaders ` -ContentType application/json $value = $listItem } elseif ($Query) { $listItems = $null $listItems = Invoke-RestMethod -Uri "$graphBaseUri/sites/$siteid/lists/$listId/items/?expand=fields&`$filter=$Query" ` -Method Get -headers $SPHeaders ` -ContentType application/json $value = @() $value = $listItems.value if ($listitems."@odata.nextLink") { $nextLink = $true } if ($nextLink) { do { $listItems = Invoke-RestMethod -Uri $listitems."@odata.nextLink"` -Method Get -headers $SPHeaders ` -ContentType application/json $value += $listItems.value if (!$listitems."@odata.nextLink") { $nextLink = $false } } until (!$nextLink) } } else { $listItems = $null $listItems = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items?expand=fields ` -Method Get -headers $SPHeaders ` -ContentType application/json $value = @() $value = $listItems.value if ($listitems."@odata.nextLink") { $nextLink = $true } if ($nextLink) { do { $listItems = Invoke-RestMethod -Uri $listitems."@odata.nextLink"` -Method Get -headers $SPHeaders ` -ContentType application/json $value += $listItems.value if (!$listitems."@odata.nextLink") { $nextLink = $false } } until (!$nextLink) } } return $value } function Set-GCITSSharePointListItem($ListId, $ItemId, $ItemObject) { $listItem = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items/$ItemId/fields ` -Method Patch -headers $SPHeaders ` -ContentType application/json ` -Body ($itemObject | ConvertTo-Json) $return = $listItem } function Get-GCITSSharePointList($ListName) { $list = Invoke-RestMethod ` -Uri "$graphBaseUri/sites/$siteid/lists?expand=columns&`$filter=displayName eq '$ListName'" ` -Headers $SPHeaders ` -ContentType "application/json" ` -Method GET $list = $list.value return $list } $appCredential = @{ AppId = $appid Secret = $secret } $accessToken = Get-GCITSAccessToken -appCredential $appCredential -tenantId $ourTenantId $SPHeaders = @{Authorization = "Bearer $accesstoken" } $headers = @{ Authorization = "Bearer $accesstoken" } $customers = @() $customers += @{ customerid = $ourtenantid defaultDomainName = $tenantDomain displayName = $tenantName } $customers += Get-GCITSMSGraphResource -Resource contracts $existingItems = $null $commonProperties = "Customer", "TenantId", "userDisplayName", "userPrincipalName", "riskEventDateTime", "riskEventDetails", "riskEventStatus", "userId", "riskLevel", "riskEventType", "createdDateTime", "closedDateTime", "riskEventId" $list = Get-GCITSSharePointList -ListName $listName if (!$list) { $columnCollection = @() foreach ($property in $commonProperties) { $type = "text" $indexed = $true if ($property -like "*DateTime") { $type = "dateTime" $indexed = $true } if ($property -eq "riskEventDetails") { $longText = $true $indexed = $false } else { $longText = $false } $columnCollection += New-GCITSSharePointColumn -Name $property -Type $type -Indexed $indexed -longText $longText } $list = New-GCITSSharePointList -Name $listName -ColumnCollection $columnCollection } else { $existingItems = Get-GCITSSharePointListItem -ListId $list.id } foreach ($customer in $customers) { Write-Host $customer.displayName $tenant_id = $customer.customerid $accessToken = Get-GCITSAccessToken -appCredential $appCredential -tenantId $tenant_id $headers = @{ Authorization = "Bearer $accessToken" } try { $identityRiskEvents = $null $identityRiskEvents = Get-GCITSMSGraphResource -Resource identityRiskEvents } catch { Write-Host "Likely not licensed for Identity Protection" } if ($identityRiskEvents -and $identityRiskEvents.riskEventType) { $objectArray = @() foreach ($riskEvent in $identityRiskEvents) { $riskEventId = $riskEvent.id $riskEvent.PSObject.Properties.Remove('id') $riskEvent | Add-Member riskEventId $riskEventId $members = $riskEvent | Get-Member | Where-Object { $_.membertype -ne "Method" -and $_.name -ne "@odata.type" } $object = [pscustomobject][ordered]@{ } $object | Add-Member Title "$($riskEvent.riskEventType) - $($customer.displayName)" $object | Add-Member Customer $customer.displayName $object | Add-Member TenantId $customer.customerId $riskEventDetails = @() foreach ($member in ($members | Where-Object { $_.name -notmatch "Customer" -and $_.name -notmatch "TenantId" })) { if ($commonProperties -contains $member.name) { if ($member.name -ne "id") { if ($member.name -like "*DateTime") { try { $dateTime = [DateTime]$riskevent.($member.name) $object | Add-Member $member.name $dateTime } catch { } } else { $object | Add-Member $member.name $riskevent.($member.name) } } else { $object | Add-Member riskEventId $riskevent.id } } else { if ($member.definition -match "Object\[\] " -or $member.Definition -match "System.Management.Automation.PSCustomObject") { $subMembers = $riskEvent.($member.name) | get-member | Where-Object { $_.membertype -ne "Method" } $stringArray = @() foreach ($subMember in $subMembers) { $stringArray += "`t$($subMember.name): $($riskEvent.$($member.name).$($subMember.name))" } $stringArray = $stringArray -join "`n" $riskEventDetails += "$($member.name): `n$stringArray" } else { $riskEventDetails += "$($member.name): $($riskEvent.$($member.name))" } } } $riskEventDetails = $riskEventDetails -join "`n" $object | Add-Member riskEventDetails $riskEventDetails $objectArray += $object } foreach ($object in $objectArray) { if ($existingItems.fields.riskEventId -contains $object.riskEventId) { $match = $existingItems | Where-Object { $_.fields.riskEventId -eq $object.riskEventId } Write-Host "Updating SharePoint list item" Set-GCITSSharePointListItem -ListId $list.id -ItemId $match.id -ItemObject $object } else { Write-Host "Adding SharePoint list item" New-GCITSSharePointListItem -ListId $list.id -ItemObject $object } } } }
Getting alerted to new Azure AD Risk Events in customer tenants
- Navigate to your new Risk Event Register in your root SharePoint Team Site via Site Contents
- Click the three dots, then Alert me
- Specify the person, group or external address who should be receiving the alerts
- Request an alert when New items are added and choose to Send notification immediately
Leave a Reply
Want to join the discussion?Feel free to contribute!