Resolve-AzureAssessPrivilegedIdentities.ps1

<#
.SYNOPSIS
    Resolve the list of identities with privileged access to resources, subscriptions and management groups.
    All identities will be saved to a csv with an indication of the source ($null = direct assignemend; guid = group providing this access).
.EXAMPLE
    Resolve-AzureAssessPrivilegedIdentities.ps1
#>


function Resolve-AzureAssessPrivilegedIdentities {
    # load the role assignements
    $roleassignmentscsv = Join-Path "." -ChildPath "roleassignments.csv"

    # path where to export identities
    $principalscsv = Join-Path -Path "." -ChildPath "principals.csv"
    $membershipscsv = Join-Path -Path "." -ChildPath "memberhips.csv"
    $ownershipscsv = Join-Path -Path "." -ChildPath "owners.csv"
    "principalId,principalType,displayName" | Out-File -FilePath $principalscsv -Force
    "Id,GroupId" | Out-File -FilePath $membershipscsv -Force
    "Id,OwnerId" | Out-File -FilePath $ownershipscsv -Force

    # get PrivilegedRoleAssignements from csvs
    $roleAssignements = get-content -Path $roleassignmentscsv | ConvertFrom-Csv

    # gather identities from role assignements
    $privIdentities = @{}
    foreach($roleAssignment in $roleAssignements) {
        $privIdentities[$roleAssignment.principalId] = $roleAssignment | Select-Object principalId,principalType,@{N="displayName";E={""}}
    }


    # get access token for graph
    $graphToken = Get-AzAccessToken -ResourceTypeName MSGraph 

    # expand groups
    # table to retain memberships
    $memberships = @()
    $queries = new-object -TypeName System.Collections.Stack
    $privIdentities.Values | where-object { $_.principalType -ieq "Group"} | Select-Object @{N="id";E={$_.principalId}},@{N="url";E={"/groups/$($_.principalId)/transitiveMembers?`$top=999&`$select=id"}} | ForEach-Object { $queries.Push($_) }
    # get token for ms graph
    $reqtoken = $graphToken.Token | ConvertTo-SecureString -AsPlainText -Force
    $count = 0
    while($queries.Count -gt 0) {
        $requests = @()
        while($requests.Count -lt 20 -and $queries.Count -gt 0) {
            $q = $queries.Pop()
            $requests += "" | select-object @{N="id"; E={$q.id}},@{N="method";E={"GET"}},@{N="url"; E={$q.url}}
        }
        $batchreq = "" | select-object @{N="requests";E={@(,$requests)}}
        $resp = Invoke-RestMethod -ContentType "application/json" -Authentication Bearer -Token $reqtoken -Method Post -Body ($batchreq | ConvertTo-Json -Compress -Depth 2) -Uri "https://graph.microsoft.com/v1.0/`$batch"
        # check for throttling
        if (($resp.responses | Where-Object {$_.status -eq 429}).Count -gt 0) {
            Start-Sleep -Seconds 60
        } else {
            Start-Sleep -Seconds 5
        }
        # requeue get failed requests
        $failedresps = $resp.responses | Where-Object { [int]($_.status/100) -ne 2 -and $_.status -ne 404} 
        foreach($failedresp in $failedresps) {
            # get corresponding request
            $req = $requests | Where-Object { $_.id -eq $failedresp.id} | Select-Object id,url
            # requeue request
            $queries.Push($req)
        }
        # parse successfull responses
        $successresps = $resp.responses | Where-Object { [int]($_.status/100) -eq 2 }
        # fill in responses
        foreach($response in $successresps) {
            foreach ($value in $response.body.value) {
                $type = "unknown"
                if ($value."@odata.type" -ieq "#microsoft.graph.user") {
                    $type = "User"
                } elseif ($value."@odata.type" -ieq "#microsoft.graph.group") {
                    $type = "Group"
                } elseif ($value."@odata.type" -ieq "#microsoft.graph.servicePrincipal") {
                    $type = "ServicePrincipal"
                }
                $memberships += "" | Select-Object @{N="Id";E={$value.id}},@{N="GroupId";E={$response.Id}}
                if ($value.id -notin $privIdentities.Keys) {
                    $privIdentities[$value.id] = "" | Select-Object @{N="principalId";E={$value.id}},@{N="principalType";E={$type}},@{N="displayName";E={""}}
                }
            } 
            if ($response."@odata.nextLink") {
                # create request for nextlink
                $req = "" | Select-Object @{N="id";E={$response.id}},@{N="url";E={$response."@odata.nextLink"}}
                # enqueue request
                $queries.Push($req)
            }
            $count += 1
        }
        Write-Progress -Activity "Getting Memberships" -Status "$count of $($count + $queries.Count)" -PercentComplete ($count * 100 / ($count + $queries.Count))
    }

    # list of ownerships
    $ownerships = @()
    # check owners of groups (has only users and serviceprincipals)
    $queries = New-Object -TypeName System.Collections.Stack
    $privIdentities.Values | where-object { $_.principalType -ieq "Group"} | ForEach-Object { $queries.Push($_.principalId) }
    # get token for ms graph
    $reqtoken = $graphToken.Token | ConvertTo-SecureString -AsPlainText -Force
    $count = 0
    while($queries.Count -gt 0) {
        $requests = @()
        while($requests.Count -lt 20 -and $queries.Count -gt 0) {
            $requests += $queries.Pop() | select-object @{N="id"; E={$_}},@{N="method";E={"GET"}},@{N="url"; E={"/groups/$($_)/owners?`$top=999&`$select=id"}}
        }
        $batchreq = "" | select-object @{N="requests";E={@(,$requests)}}
        $resp = Invoke-RestMethod -ContentType "application/json" -Authentication Bearer -Token $reqtoken -Method Post -Body ($batchreq | ConvertTo-Json -Compress -Depth 2) -Uri "https://graph.microsoft.com/v1.0/`$batch"
        # check for throttling
        if (($resp.responses | Where-Object {$_.status -eq 429}).Count -gt 0) {
            Start-Sleep -Seconds 60
        } else {
            Start-Sleep -Seconds 5
        }
        # requeue get failed requests
        $resp.responses | Where-Object { [int]($_.status/100) -ne 2 -and $_.status -ne 404} | ForEach-Object { $queries.Push($_.id) }
        # parse successfull responses
        $successresps = $resp.responses | Where-Object { [int]($_.status/100) -eq 2 }
        # fill in responses
        foreach($response in $successresps) {
            foreach ($value in $response.body.value) {
                $type = "unknown"
                if ($value."@odata.type" -ieq "#microsoft.graph.user") {
                    $type = "User"
                } elseif ($value."@odata.type" -ieq "#microsoft.graph.servicePrincipal") {
                    $type = "ServicePrincipal"
                }
                $ownerships += "" | Select-Object @{N="Id";E={$response.id}},@{N="OwnerId";E={$value.Id}}
                if ($value.id -notin $privIdentities.Keys) {
                    $privIdentities[$value.id] = "" | Select-Object @{N="principalId";E={$value.id}},@{N="principalType";E={$type}},@{N="displayName";E={""}}
                }
            } 
            if ($response."@odata.nextLink") {
                # create request for nextlink
                $req = "" | Select-Object @{N="id";E={$response.id}},@{N="url";E={$response."@odata.nextLink"}}
                # enqueue request
                $queries.Push($req)
            }
            $count += 1
        }
        Write-Progress -Activity "Getting Group Owners" -Status "$count of $($count + $queries.Count)" -PercentComplete ($count * 100 / ($count + $queries.Count))
    }


    # owner check for serviceprincipals
    # TODO - handle potential of long nesting
    $queries = New-Object -TypeName System.Collections.Stack
    $privIdentities.Values | Where-Object { $_.principalType -eq "ServicePrincipal" } | ForEach-Object { $queries.Push($_.principalId)}
    $count = 0
    while ($queries.Count -gt 0) {
        $requests = @()
        while($requests.Count -lt 20 -and $queries.Count -gt 0) {
            $requests += $queries.Pop() | select-object @{N="id"; E={$_}},@{N="method";E={"GET"}},@{N="url"; E={"/servicePrincipals/$($_)/owners?`$top=999&`$select=id"}}
        }
        $batchreq = "" | select-object @{N="requests";E={@(,$requests)}}
        $resp = Invoke-RestMethod -ContentType "application/json" -Authentication Bearer -Token $reqtoken -Method Post -Body ($batchreq | ConvertTo-Json -Compress -Depth 2) -Uri "https://graph.microsoft.com/v1.0/`$batch"
        # check for throttling
        if (($resp.responses | Where-Object {$_.status -eq 429}).Count -gt 0) {
            Start-Sleep -Seconds 60
        } else {
            Start-Sleep -Seconds 5
        }
        # add requests havint returned not found to the counter
        $count += ($resp.responses | Where-Object { $_.status -eq 404 }).Count
        # requeue get failed requests
        $resp.responses | Where-Object { [int]($_.status/100) -ne 2 -and $_.status -ne 404} | ForEach-Object { $queries.Push($_.id) }
        # parse successfull responses
        $successresps = $resp.responses | Where-Object { [int]($_.status/100) -eq 2 }
        # fill in responses
        foreach($response in $successresps) {
            foreach ($value in $response.body.value) {
                $type = "unknown"
                if ($value."@odata.type" -ieq "#microsoft.graph.user") {
                    $type = "User"
                } elseif ($value."@odata.type" -ieq "#microsoft.graph.servicePrincipal") {
                    $type = "ServicePrincipal"
                    if ($value.id -ne $response.id -and $value.id -notin $privIdentities.Keys) {
                        $queries.Push($value.id)
                    }
                }
                $ownerships += "" | Select-Object @{N="Id";E={$response.id}},@{N="OwnerId";E={$value.Id}}
                if ($value.id -notin $privIdentities.Keys) {
                    $privIdentities[$value.id] = "" | Select-Object @{N="principalId";E={$value.id}},@{N="principalType";E={$type}},@{N="displayName";E={""}}
                }
                $count += 1
            } 
            if ($response."@odata.nextLink") {
                # create request for nextlink
                $req = "" | Select-Object @{N="id";E={$response.id}},@{N="url";E={$response."@odata.nextLink"}}
                # enqueue request
                $queries.Push($req)
            }
        }
        Write-Progress -Activity "Getting ServicePrincipals Owners" -Status "$count of $($count + $queries.Count)" -PercentComplete ($count * 100 / ($count + $queries.Count))
    }

    # resolve privilegedIdentities displaynames
    $directoryobjects = @{
        ids = @($privIdentities.Keys)
        types = @(
            "user"
            "group"
            "serviceprincipal"
        )
    } | ConvertTo-Json -Depth 2 -Compress
    $res = Invoke-RestMethod -ContentType "application/json" -Authentication Bearer -Token $reqtoken -Method Post -Body $directoryobjects -Uri "https://graph.microsoft.com/v1.0/directoryObjects/getByIds?`$select=id,displayName"
    Start-Sleep -Seconds 5
    foreach($value in $res.value) {
        $privIdentities[$value.id].displayName = $value.displayName
    }
    foreach($id in $privIdentities.Keys) {
        if ($privIdentities[$id].displayName -eq "") {
            $privIdentities[$id].displayName = $id
        }
    }

    # save extracted information
    $privIdentities.Values | Export-Csv -Path $principalscsv -NoTypeInformation -Append
    $memberships | Export-Csv -Path $membershipscsv -NoTypeInformation -Append
    $ownerships | Export-Csv -Path $ownershipscsv -NoTypeInformation -Append

    # path to export user details
    $userscsv = Join-Path -Path "." -ChildPath "users.csv"
    # path to export group details
    $groupscsv = Join-Path -Path "." -ChildPath "groups.csv"
    # path to export service principal details
    $serviceprincipalscsv = Join-Path -Path "." -ChildPath "serviceprincipals.csv"

    # check group details
    # create file and headers
    "id,displayName,mailEnabled,securityEnabled,onPremisesSyncEnabled" | Out-File -FilePath $groupscsv -Force
    # get groups informations
    $queries = New-Object -TypeName System.Collections.Stack
    $privIdentities.Values | Where-Object { $_.principalType -eq "Group" } | ForEach-Object { $queries.Push($_.principalId) }
    $count = 0
    while($queries.Count -gt 0) {
        $requests = @()
        while($requests.Count -lt 20 -and $queries.Count -gt 0) {
            $groupid = $queries.Pop()
            $requests += "" | select-object @{N="id"; E={$groupid}},@{N="method";E={"GET"}},@{N="url"; E={"/groups/$($groupid)?`$select=id,displayName,mailEnabled,securityEnabled,onPremisesSyncEnabled"}}
        }
        $batchreq = "" | select-object @{N="requests";E={@(,$requests)}}
        $resp = Invoke-RestMethod -ContentType "application/json" -Authentication Bearer -Token $reqtoken -Method Post -Body ($batchreq | ConvertTo-Json -Compress -Depth 2) -Uri "https://graph.microsoft.com/v1.0/`$batch"
        # check for throttling
        if (($resp.responses | Where-Object {$_.status -eq 429}).Count -gt 0) {
            Start-Sleep -Seconds 60
        } else {
            Start-Sleep -Seconds 5
        }
        # requeue get failed requests
        $resp.responses | Where-Object { [int]($_.status/100) -ne 2 -and $_.status -ne 404} | ForEach-Object { $queries.Push($_.id) } 
        # fill in responses
        $resp.responses | Where-Object { [int]($_.status/100) -eq 2 } | ForEach-Object{ $count += 1; $_.body } | Select-Object id,displayName,mailEnabled,securityEnabled,onPremisesSyncEnabled | Export-Csv -Path $groupscsv -NoTypeInformation -Append
        Write-Progress -Activity "Getting Groups" -Status "$count of $($count + $queries.Count)" -PercentComplete ($count * 100 / ($count + $queries.Count))
    }

    # check service principals details
    $validitydate = get-date
    $appqueries = New-Object -TypeName System.Collections.Stack
    # create file and headers
    "id,appId,displayName,servicePrincipalType,expiredPasswordCredentials,validPasswordCredentials,expiredKeyCredentials,validKeyCredentials,federatedIdentityCredentials,accountEnabled,lastSignIn,lastAzureSignIn" | Out-File -FilePath $serviceprincipalscsv -Force
    $serviceprincipals = @{}
    # get service principals informations
    $queries = New-Object -TypeName System.Collections.Stack
    $privIdentities.Values | Where-Object { $_.principalType -eq "ServicePrincipal" } | ForEach-Object { $queries.Push($_.principalId) }
    $count = 0
    while($queries.Count -gt 0) {
        $requests = @()
        while($requests.Count -lt 20 -and $queries.Count -gt 0) {
            $spid = $queries.Pop()
            $requests += "" | select-object @{N="id"; E={$spid}},@{N="method";E={"GET"}},@{N="url"; E={"/servicePrincipals/$($spid)?`$select=id,appId,displayName,servicePrincipalType,passwordCredentials,keyCredentials,federatedIdentityCredentials,accountEnabled"}}
        }
        $batchreq = "" | select-object @{N="requests";E={@(,$requests)}}
        $resp = Invoke-RestMethod -ContentType "application/json" -Authentication Bearer -Token $reqtoken -Method Post -Body ($batchreq | ConvertTo-Json -Compress -Depth 2) -Uri "https://graph.microsoft.com/v1.0/`$batch"
        # check for throttling
        if (($resp.responses | Where-Object {$_.status -eq 429}).Count -gt 0) {
            Start-Sleep -Seconds 60
        } else {
            Start-Sleep -Seconds 5
        }
        # requeue get failed requests
        $resp.responses | Where-Object { [int]($_.status/100) -ne 2 -and $_.status -ne 404 } | ForEach-Object { $queries.Push($_.id) }
        # fill app queries
        $resp.responses | Where-Object { [int]($_.status/100) -eq 2 } | ForEach-Object{ $appqueries.Push($_.body.appId) }
        # fill in responses
        $resp.responses | Where-Object { [int]($_.status/100) -eq 2 } | ForEach-Object{ $count += 1; $_.body } `
            | Select-Object `
                id, `
                appId, `
                displayName, `
                servicePrincipalType, `
                @{N="expiredPasswordCredentials";E={@($_.passwordCredentials | Where-Object {$_.endDateTime -le $validitydate}).Count}}, `
                @{N="validPasswordCredentials";E={@($_.passwordCredentials | Where-Object {$_.endDateTime -gt $validitydate}).Count}}, `
                @{N="expiredKeyCredentials";E={@($_.keyCredentials | Where-Object {$_.endDateTime -le $validitydate}).Count}}, `
                @{N="validKeyCredentials";E={@($_.keyCredentials | Where-Object {$_.endDateTime -le $validitydate}).Count}}, `
                @{N="federatedIdentityCredentials";E={$_.federatedIdentityCredentials.Count}},accountEnabled, `
                @{N="lastAzureSignIn";E={$null}}, `
                @{N="lastSignIn";E={$null}} `
            | ForEach-Object { $serviceprincipals[$_.appId] = $_}
        Write-Progress -Activity "Getting ServicePrincipals" -Status "$count of $($count + $queries.Count)" -PercentComplete ($count * 100 / ($count + $queries.Count))
    }

    # get app infromations
    $count = 0
    while($appqueries.Count -gt 0) {
        $requests = @()
        while($requests.Count -lt 20 -and $appqueries.Count -gt 0) {
            $appid = $appqueries.Pop()
            $requests += "" | select-object @{N="id"; E={$appid}},@{N="method";E={"GET"}},@{N="url"; E={"/applications?`$filter=appId+eq+'$appid'&`$select=id,appId,displayName,keyCredentials,passwordCredentials,federatedIdentityCredentials"}}
        }
        $batchreq = "" | select-object @{N="requests";E={@(,$requests)}}
        $resp = Invoke-RestMethod -ContentType "application/json" -Authentication Bearer -Token $reqtoken -Method Post -Body ($batchreq | ConvertTo-Json -Compress -Depth 2) -Uri "https://graph.microsoft.com/v1.0/`$batch"
        # check for throttling
        if (($resp.responses | Where-Object {$_.status -eq 429}).Count -gt 0) {
            Start-Sleep -Seconds 60
        } else {
            Start-Sleep -Seconds 5
        }
        # requeue get failed requests
        $resp.responses | Where-Object { [int]($_.status/100) -ne 2 -and $_.status -ne 404} | ForEach-Object { $queries.Push($_.id) }
        # udpate service principals
        $resp.responses | Where-Object { [int]($_.status/100) -eq 2 } | ForEach-Object{ $_.body.value } `
            | Select-Object id,appId,displayName,@{N="expiredPasswordCredentials";E={@($_.passwordCredentials | Where-Object {$_.endDateTime -le $validitydate}).Count}},@{N="validPasswordCredentials";E={@($_.passwordCredentials | Where-Object {$_.endDateTime -gt $validitydate}).Count}},@{N="expiredKeyCredentials";E={@($_.keyCredentials | Where-Object {$_.endDateTime -le $validitydate}).Count}},@{N="validKeyCredentials";E={@($_.keyCredentials | Where-Object {$_.endDateTime -le $validitydate}).Count}},@{N="federatedIdentityCredentials";E={$_.federatedIdentityCredentials.Count}} `
            | ForEach-Object {
                $count += 1 
                $serviceprincipals[$_.appId].expiredKeyCredentials += $_.expiredKeyCredentials
                $serviceprincipals[$_.appId].validKeyCredentials += $_.validKeyCredentials
                $serviceprincipals[$_.appId].expiredPasswordCredentials += $_.expiredPasswordCredentials
                $serviceprincipals[$_.appId].validPasswordCredentials += $_.validPasswordCredentials
                $serviceprincipals[$_.appId].federatedIdentityCredentials += $_.federatedIdentityCredentials
            }
        Write-Progress -Activity "Getting Applications" -Status "$count of $($count + $appqueries.Count)" -PercentComplete ($count * 100 / ($count + $appqueries.Count))
    }
    
    # check user details
    # create file and headers
    "id,displayName,userPrincipalName,mail,accountEnabled,department,userType,onPremisesSyncEnabled,lastSignIn,lastAzureSignIn" | Out-File -FilePath $userscsv -Force
    # get user informations
    $queries = New-Object -TypeName System.Collections.Stack
    $privIdentities.Values | Where-Object { $_.principalType -eq "User" } | ForEach-Object { $queries.Push($_.principalId) }
    $count = 0
    $users = @()
    while($queries.Count -gt 0) {
        $requests = @()
        while($requests.Count -lt 20 -and $queries.Count -gt 0) {
            $userid = $queries.Pop()
            $requests += "" | select-object @{N="id"; E={$userid}},@{N="method";E={"GET"}},@{N="url"; E={"/users/$($userid)?`$select=id,displayName,userPrincipalName,mail,accountEnabled,department,userType,signInActivity,onPremisesSyncEnabled"}}
        }
        $batchreq = "" | select-object @{N="requests";E={@(,$requests)}}
        $resp = Invoke-RestMethod -ContentType "application/json" -Authentication Bearer -Token $reqtoken -Method Post -Body ($batchreq | ConvertTo-Json -Compress -Depth 2) -Uri "https://graph.microsoft.com/beta/`$batch"
        # check for throttling
        if (($resp.responses | Where-Object {$_.status -eq 429}).Count -gt 0) {
            Start-Sleep -Seconds 60
        } else {
            Start-Sleep -Seconds 5
        }
        # requeue get failed requests
        $resp.responses | Where-Object { [int]($_.status/100) -ne 2 -and $_.status -ne 404} | ForEach-Object { $queries.Push($_.id) } 
        # fill in responses
        $users += $resp.responses | Where-Object { [int]($_.status/100) -eq 2 } | ForEach-Object{ $count += 1; $_.body } | Select-Object `
            id, `
            displayName, `
            userPrincipalName, `
            mail,`
            accountEnabled,`
            department,`
            userType,`
            @{N="onPremisesSyncEnabled";E={$_.onPremisesSyncEnabled -eq $true}}, `
            @{N="lastSignIn";E={$_.signInActivity.lastSuccessfulSignInDateTime}}, `
            @{N="lastAzureSignIn";E={$_.signInActivity.lastSuccessfulSignInDateTime}}
        Write-Progress -Activity "Getting Users" -Status "$count of $($count + $queries.Count)" -PercentComplete ($count * 100 / ($count + $queries.Count))
    }


    # determine if there is a log analytics to get details
    # looking at Azure AD diagnostic settings
    $res = Invoke-AzRestMethod -Method "GET" -path "/providers/microsoft.aadiam/diagnosticSettings/?api-version=2017-04-01"
    $aaddiagsettings = @()
    if ($res.StatusCode -eq 200) {
        $aaddiagsettings += ($res.Content | convertfrom-json).value | Select-Object -ExpandProperty properties `
            | Where-Object { 
                ![string]::IsNullorEmpty($_.workspaceId) -and
                ($_.logs | Where-Object { $_.category -eq "SignInLogs" -and $_.enabled }).count -gt 0 -and # User SignIn logs
                ($_.logs | Where-Object { $_.category -eq "NonInteractiveUserSignInLogs" -and $_.enabled }).count -gt 0 # User Non Interactive SignIn logs
            } `
            | Select-Object workspaceId,@{N="kind";E={"user"}},@{N="retention";E={($_.logs | Where-Object { $_.category -eq "SignInLogs" -and $_.enabled }).retentionPolicy.days}}
        $aaddiagsettings += ($res.Content | convertfrom-json).value | Select-Object -ExpandProperty properties `
            | Where-Object { 
                ![string]::IsNullorEmpty($_.workspaceId) -and
                ($_.logs | Where-Object { $_.category -eq "ServicePrincipalSignInLogs" -and $_.enabled }).count -gt 0 -and # SPN SignIn logs
                ($_.logs | Where-Object { $_.category -eq "ManagedIdentitySignInLogs" -and $_.enabled }).count -gt 0 # MI SignIn logs
            } `
            | Select-Object workspaceId,@{N="kind";E={"spn"}},@{N="retention";E={($_.logs | Where-Object { $_.category -eq "ServicePrincipalSignInLogs" -and $_.enabled }).retentionPolicy.days}}
    }
    $userworkspaceId = $aaddiagsettings | Where-Object { $_.kind -eq "user" } | Sort-Object -Property retention -Descending | Select-Object -First 1 -ExpandProperty workspaceId
    $spnworkspaceId = $aaddiagsettings | Where-Object { $_.kind -eq "spn" } | Sort-Object -Property retention -Descending | Select-Object -First 1 -ExpandProperty workspaceId

    # get user last signin to Azure with a maxspan of last six months
    if (![string]::IsNullorEmpty($userworkspaceId)) {
        $sub = ($userworkspaceId -split "/")[2]
        $rg = ($userworkspaceId -split "/")[4]
        $name = ($userworkspaceId -split "/")[-1]
        $kql = "
        SigninLogs
        | where ResourceDisplayName == 'Windows Azure Service Management API' and TimeGenerated > ago(6*30d) and ResultType == 0
        | project TimeGenerated,UserId
        | summarize TimeGenerated=max(TimeGenerated) by UserId
        | union (
            AADNonInteractiveUserSignInLogs
            | where ResourceDisplayName == 'Windows Azure Service Management API' and TimeGenerated > ago(6*30d) and ResultType == 0
            | project TimeGenerated,UserId
            | summarize TimeGenerated=max(TimeGenerated) by UserId
        )
        | summarize TimeGenerated=max(TimeGenerated) by UserId
        "

        $count = 0
        Write-Progress -Activity "Check Users last signing to Azure" -Status "$count of $($users.Count)" -PercentComplete ($count * 100 / $users.Count)
        Set-AzContext -Subscription $sub | Out-Null
        $ws = Get-AzOperationalInsightsWorkspace -Name $name -ResourceGroupName $rg
        $query = Invoke-AzOperationalInsightsQuery -WorkspaceId $ws.CustomerId -Query $kql
        foreach($user in $users) {
            $timegenerated = ($query.Results | Where-Object { $_.UserId -eq $user.id }).TimeGenerated
            if (![string]::IsNullorEmpty($timegenerated)) {
                $user.lastAzureSignIn = get-date -date $timegenerated
            }
            $count += 1
            Write-Progress -Activity "Check Users last signing to Azure" -Status "$count of $($users.Count)" -PercentComplete ($count * 100 / $users.Count)
        }
    }

    # get serviceprincipal last signin to Azure with a maxspan of last six months
    if (![string]::IsNullorEmpty($userworkspaceId)) {
        $sub = ($spnworkspaceId -split "/")[2]
        $rg = ($spnworkspaceId -split "/")[4]
        $name = ($spnworkspaceId -split "/")[-1]
        $kql = "
        AADServicePrincipalSignInLogs
        | where TimeGenerated > ago(6*30d) and ResultType == 0
        | project TimeGenerated,ResourceDisplayName,AppId
        | summarize LastAzureSignIn=maxif(TimeGenerated,ResourceDisplayName == 'Windows Azure Service Management API'),LastSignIn=max(TimeGenerated) by AppId
        | union (
            AADManagedIdentitySignInLogs
            | where TimeGenerated > ago(6*30d) and ResultType == 0
            | project TimeGenerated,ResourceDisplayName,AppId
        | summarize LastAzureSignIn=maxif(TimeGenerated,ResourceDisplayName == 'Windows Azure Service Management API'),LastSignIn=max(TimeGenerated) by AppId
        )
        | summarize LastAzureSignIn=max(LastAzureSignIn),LastSignIn=max(LastSignIn) by AppId
        "

        $count = 0
        Write-Progress -Activity "Check ServicePrincipal last signing to Azure" -Status "$count of $($serviceprincipals.Keys.Count)" -PercentComplete ($count * 100 / $serviceprincipals.Keys.Count)
        Set-AzContext -Subscription $sub | Out-Null
        $ws = Get-AzOperationalInsightsWorkspace -Name $name -ResourceGroupName $rg
        $query = Invoke-AzOperationalInsightsQuery -WorkspaceId $ws.CustomerId -Query $kql
        foreach($appid in $serviceprincipals.Keys) {
            $spninfo = $query.Results | Where-Object { $_.AppId -eq $appid }
            $lastsignin = $spninfo.LastSignIn
            $lastazuresignin = $spninfo.LastAzureSignIn
            if (![string]::IsNullorEmpty($lastazuresignin)) {
                $serviceprincipals[$appid].lastAzureSignIn = get-date -date $lastazuresignin
            }
            $lastazuresignin = $spninfo.LastSignIn
            if (![string]::IsNullorEmpty($lastsignin)) {
                $serviceprincipals[$appid].lastSignIn = get-date -date $lastsignin
            }
            $count += 1
            Write-Progress -Activity "Check ServicePrincipal last signing to Azure" -Status "$count of $($serviceprincipals.Keys.Count)" -PercentComplete ($count * 100 / $serviceprincipals.Keys.Count)
        }
    }

    # export users to csv
    $users |  Export-Csv -Path $userscsv -NoTypeInformation -Append
    # save service principals
    $serviceprincipals.Values | Export-Csv -Path $serviceprincipalscsv -NoTypeInformation -Append

    # do not extend if memberships are already found
    if (($roleAssignements | Where-Object { $_.source -eq "Group"}).Count -eq 0) {
        # extend roleassignments with memberships
        $count = 0
        foreach($membership in $memberships) {
            $roleAssignements += @($roleAssignements | Where-Object {$_.principalId -eq $membership.GroupId}) `
                | Select-Object `
                    "roleId", `
                    "role", `
                    @{N="principalId";E={$membership.Id}}, `
                    @{N="principalType";E={$privIdentities[$membership.Id].principalType}}, `
                    "scope", `
                    @{N="source";E={"Group"}},`
                    "resourceType", `
                    "resourceName", `
                    "link"
            $count += 1
            Write-Progress -Activity "Expand Role Assignment with Memberships" -Status "$count of $($memberships.Count)" -PercentComplete ($count * 100 / $memberships.Count)
        }
    }

    # do not extend if ownerhips are already found
    if (($roleAssignements | Where-Object { $_.source -eq "Owner"}).Count -eq 0) {
        $count = 0
        # extend roleassignments with ownerships
        foreach($ownership in $ownerships) {            
            $roleAssignements += @($roleAssignements | Where-Object {$_.principalId -eq $ownership.Id}) `
                | Select-Object `
                    "roleId", `
                    "role", `
                    @{N="principalId";E={$ownership.OwnerId}}, `
                    @{N="principalType";E={$privIdentities[$ownership.OwnerId].principalType}}, `
                    "scope", `
                    @{N="source";E={"Owner"}},`
                    "resourceType", `
                    "resourceName", `
                    "link"
            $count += 1
            Write-Progress -Activity "Expand Role Assignment with Ownerships" -Status "$count of $($ownerships.Count)" -PercentComplete ($count * 100 / $ownerships.Count)
        }
    }

    $roleAssignements | export-csv -Path $roleassignmentscsv -NoTypeInformation -Force
}
# SIG # Begin signature block
# MIIRWAYJKoZIhvcNAQcCoIIRSTCCEUUCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCpjihNGISVYTjN
# r5F/RbuGzClKTPYs6LHiFsQ7e3yMUKCCDZIwgga5MIIEoaADAgECAhEAmaOACiZV
# O2Wr3G6EprPqOTANBgkqhkiG9w0BAQwFADCBgDELMAkGA1UEBhMCUEwxIjAgBgNV
# BAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAlBgNVBAsTHkNlcnR1bSBD
# ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMbQ2VydHVtIFRydXN0ZWQg
# TmV0d29yayBDQSAyMB4XDTIxMDUxOTA1MzIxOFoXDTM2MDUxODA1MzIxOFowVjEL
# MAkGA1UEBhMCUEwxITAfBgNVBAoTGEFzc2VjbyBEYXRhIFN5c3RlbXMgUy5BLjEk
# MCIGA1UEAxMbQ2VydHVtIENvZGUgU2lnbmluZyAyMDIxIENBMIICIjANBgkqhkiG
# 9w0BAQEFAAOCAg8AMIICCgKCAgEAnSPPBDAjO8FGLOczcz5jXXp1ur5cTbq96y34
# vuTmflN4mSAfgLKTvggv24/rWiVGzGxT9YEASVMw1Aj8ewTS4IndU8s7VS5+djSo
# McbvIKck6+hI1shsylP4JyLvmxwLHtSworV9wmjhNd627h27a8RdrT1PH9ud0IF+
# njvMk2xqbNTIPsnWtw3E7DmDoUmDQiYi/ucJ42fcHqBkbbxYDB7SYOouu9Tj1yHI
# ohzuC8KNqfcYf7Z4/iZgkBJ+UFNDcc6zokZ2uJIxWgPWXMEmhu1gMXgv8aGUsRda
# CtVD2bSlbfsq7BiqljjaCun+RJgTgFRCtsuAEw0pG9+FA+yQN9n/kZtMLK+Wo837
# Q4QOZgYqVWQ4x6cM7/G0yswg1ElLlJj6NYKLw9EcBXE7TF3HybZtYvj9lDV2nT8m
# FSkcSkAExzd4prHwYjUXTeZIlVXqj+eaYqoMTpMrfh5MCAOIG5knN4Q/JHuurfTI
# 5XDYO962WZayx7ACFf5ydJpoEowSP07YaBiQ8nXpDkNrUA9g7qf/rCkKbWpQ5bou
# fUnq1UiYPIAHlezf4muJqxqIns/kqld6JVX8cixbd6PzkDpwZo4SlADaCi2JSplK
# ShBSND36E/ENVv8urPS0yOnpG4tIoBGxVCARPCg1BnyMJ4rBJAcOSnAWd18Jx5n8
# 58JSqPECAwEAAaOCAVUwggFRMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFN10
# XUwA23ufoHTKsW73PMAywHDNMB8GA1UdIwQYMBaAFLahVDkCw6A/joq8+tT4HKbR
# Og79MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEFBQcDAzAwBgNVHR8E
# KTAnMCWgI6Ahhh9odHRwOi8vY3JsLmNlcnR1bS5wbC9jdG5jYTIuY3JsMGwGCCsG
# AQUFBwEBBGAwXjAoBggrBgEFBQcwAYYcaHR0cDovL3N1YmNhLm9jc3AtY2VydHVt
# LmNvbTAyBggrBgEFBQcwAoYmaHR0cDovL3JlcG9zaXRvcnkuY2VydHVtLnBsL2N0
# bmNhMi5jZXIwOQYDVR0gBDIwMDAuBgRVHSAAMCYwJAYIKwYBBQUHAgEWGGh0dHA6
# Ly93d3cuY2VydHVtLnBsL0NQUzANBgkqhkiG9w0BAQwFAAOCAgEAdYhYD+WPUCia
# U58Q7EP89DttyZqGYn2XRDhJkL6P+/T0IPZyxfxiXumYlARMgwRzLRUStJl490L9
# 4C9LGF3vjzzH8Jq3iR74BRlkO18J3zIdmCKQa5LyZ48IfICJTZVJeChDUyuQy6rG
# DxLUUAsO0eqeLNhLVsgw6/zOfImNlARKn1FP7o0fTbj8ipNGxHBIutiRsWrhWM2f
# 8pXdd3x2mbJCKKtl2s42g9KUJHEIiLni9ByoqIUul4GblLQigO0ugh7bWRLDm0Cd
# Y9rNLqyA3ahe8WlxVWkxyrQLjH8ItI17RdySaYayX3PhRSC4Am1/7mATwZWwSD+B
# 7eMcZNhpn8zJ+6MTyE6YoEBSRVrs0zFFIHUR08Wk0ikSf+lIe5Iv6RY3/bFAEloM
# U+vUBfSouCReZwSLo8WdrDlPXtR0gicDnytO7eZ5827NS2x7gCBibESYkOh1/w1t
# VxTpV2Na3PR7nxYVlPu1JPoRZCbH86gc96UTvuWiOruWmyOEMLOGGniR+x+zPF/2
# DaGgK2W1eEJfo2qyrBNPvF7wuAyQfiFXLwvWHamoYtPZo0LHuH8X3n9C+xN4YaNj
# t2ywzOr+tKyEVAotnyU9vyEVOaIYMk3IeBrmFnn0gbKeTTyYeEEUz/Qwt4HOUBCr
# W602NCmvO1nm+/80nLy5r0AZvCQxaQ4wggbRMIIEuaADAgECAhBNn7NsFkgeJAAl
# SKxTsSjWMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhB
# c3NlY28gRGF0YSBTeXN0ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBDb2RlIFNp
# Z25pbmcgMjAyMSBDQTAeFw0yNDAxMTYxMjAwMzRaFw0yNTAxMTUxMjAwMzNaMHcx
# CzAJBgNVBAYTAkJFMRgwFgYDVQQIDA9XYWxsb29uIEJyYWJhbnQxHjAcBgNVBAoM
# FU9wZW4gU291cmNlIERldmVsb3BlcjEuMCwGA1UEAwwlT3BlbiBTb3VyY2UgRGV2
# ZWxvcGVyLCBDZWRyaWMgQmxvbWFydDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC
# AgoCggIBALp1WzPLBGmSjvJ2se9K+HWIzxlPIh9vmddaj0Sc6Yz3jJizVN7WphR7
# 1kR4KFEfCFL6eleZlIkqDMkNfwBTpfVCx7MYR85YSceHo8ieEIV5MmWSZ1CR36/A
# wXrUvXzlX85Rf/RuotfluSmdhTRoBbCN9aIsSm50pEZixvUOcipTNZbuY29NvjXP
# JAH1RX0od6QYCGt/v+5C+hBippk+QohxmKQYLilh4+58fdZvnXaKPvEuEpbhXCeh
# /HbASrvTbZXXbh8fMbHOq+xnCemK/6aqjfqgRzkXn9unjl6wj6KT7MoEp1tCn+9z
# cmi/zW3KeI3Alld5B6IEvVfZVmaCrvSsRBNFnVpVg5eGQNRyan0xfblEwKLrsoNk
# WhUtJY3SD53kvz8OHBKRY1quaq70UefcIDuPq4+76CVovjbpT/wnmP6PFnHtNrDj
# KzMAnUWOeseIcDhp7ETzVyoh2rWofTpXUr5aEau5mkXs0MclqjimMeWf0r3NyBa5
# E3k0rG/xtpmTFB9e4w7b0VQUxNUcVdmocGL7Rj4ouWLjUDUkiE2DctxMTaPmqKJE
# nx8cFsBCrDmgNYxGOZ03ykPmPbwc3SooZeS5rRmR4v2GTVrbsmbiiggtYDjhJddo
# sC/TtJ+65JL48oTHDK1KVMtSjUBr/Q16Nzg0JaXfiZ4JS+MX45ylAgMBAAGjggF4
# MIIBdDAMBgNVHRMBAf8EAjAAMD0GA1UdHwQ2MDQwMqAwoC6GLGh0dHA6Ly9jY3Nj
# YTIwMjEuY3JsLmNlcnR1bS5wbC9jY3NjYTIwMjEuY3JsMHMGCCsGAQUFBwEBBGcw
# ZTAsBggrBgEFBQcwAYYgaHR0cDovL2Njc2NhMjAyMS5vY3NwLWNlcnR1bS5jb20w
# NQYIKwYBBQUHMAKGKWh0dHA6Ly9yZXBvc2l0b3J5LmNlcnR1bS5wbC9jY3NjYTIw
# MjEuY2VyMB8GA1UdIwQYMBaAFN10XUwA23ufoHTKsW73PMAywHDNMB0GA1UdDgQW
# BBQ2dBi+EnTsl++Xmrax5h7dyzC12TBLBgNVHSAERDBCMAgGBmeBDAEEATA2Bgsq
# hGgBhvZ3AgUBBDAnMCUGCCsGAQUFBwIBFhlodHRwczovL3d3dy5jZXJ0dW0ucGwv
# Q1BTMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG
# 9w0BAQsFAAOCAgEAZm3wL05QNFh25ll601jtST553L2VVS4ugvlHmX90PSOkjmX3
# jQ2ERwdz9MHWhoOvW1EdOrBlm31YRt2GmCz4/82vq9fTTNwLSzk3csHEIgHuAbN1
# INCVgz0l7Ya7mkxFLHoZ4BJ9LluS4p2NjvU9r7OGnBCgfFaLriC+qRY8QxCeMQOl
# l1BTTaUBMp0pgxh3XOOxOQfNeFCkXg0q4QULkaRuApJFZi/pY+PMA6p8bjOTP4YI
# 9VNsoc7ReBYMDL588oupc1CTiDaf0e6YaN80z6WoIGuNja3VPDEG4VmWZG+Of9gO
# XUNllvR8n1IXXsvWEuHRTCV9jjK89NHGaNroeXHr1C6eERnoNUPbEuIb/parUnGB
# n5MKWL+2TlL9Z34lSFz6e1Efi4oJeDznUojNZKJzOMvBS+JJyr0aVuW4lDoqbqkl
# 9vFLjHi1oGM7mBrlL8AHEC3iUNEOwgPcbsIYAqV85RyXYNQqLu1ik1MmmpKy/Tma
# Mmi/AiXxtjC6RrnrkN6tIHR7Nky9b5jWfMxCCG2b119gr7SkJ8qVOH8I9oBxMDPR
# IjBi5rEjSbDOCpRhZiqmx7K7gW2Gyp1OCIffVyiqmeZh+YLxpdP0tymxtqg/HVqV
# If3xn0Sgl7HcUpcRkY3EgTPUqNaVzOSO5h8KyzA9nVtUX2XwSx9C4vb6uqcxggMc
# MIIDGAIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBT
# eXN0ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBDb2RlIFNpZ25pbmcgMjAyMSBD
# QQIQTZ+zbBZIHiQAJUisU7Eo1jANBglghkgBZQMEAgEFAKCBhDAYBgorBgEEAYI3
# AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisG
# AQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCDTfTR9rMiL
# vQ7KLebvohR5E9rwit3sa9C92iKQFGy88DANBgkqhkiG9w0BAQEFAASCAgCJVWev
# 6vXYLrfCaV/ieFvn8aAiiZwXDqozKv6jWc44AZV21KNHIdQKwiJKp0uzdn6BDjnR
# yo0d3Rr5jS3F/QzhuF2+1o+Q9CWGJiwcaCkZvpT9c/gAaIgr3NcKWpk3zYf8EFSA
# 8seNuiYN9pKNLn9FJpqDPatI1F8F9ukwkyZHYxBlsRs9px6zALZp+Dp8SIfYbryq
# e8p0DzjB/i0wSgGvWO3jAEHy9ePqPqQ6YJ9jqr71faKz645iGLPOq3Rudp05aWRo
# giugUvjEgt43XGKPRhWgfqe/sNsD8jtwtFlY/5izGZLBuIWaBKOfUlmE0GU0+Q0j
# ENsGhK/14G7cRKU03nSeZyvP6gBMxa8StO7NYhPWRPx1X2iLkc4SsvPL0KEkGBug
# Y3x4WRVrNbXfN+IPn3il4RP6pvqW3BBbGiRqZvL7i/DQjfAMxQ7dqa4gB0jJhVbk
# MJcdY4QQtVTZJASbaHTnxIVU2KVEijPTiOl+AG4/LaS0uBgY9ASJOl6NC7uQoDNG
# X/g5izKn7YfLCDVIdAxcb4yvxVR2+1pXNgcsCgmWxxR/iLB5pm4/lVn2j3ttodIB
# QxmlrQmgats3kR33t9KbjTW0XtA1h256NcJrvsExLqqGRe8aVzJA9PyjQ4UlBQSM
# 5tZ3+oIaHuUbLlD/gDau2b6+xuhh9Q+SJtz1AA==
# SIG # End signature block