diff --git a/Scripts/RemediationScripts/Remediate-InvalidAADObjectRoleAssignments.ps1 b/Scripts/RemediationScripts/Remediate-InvalidAADObjectRoleAssignments.ps1 index 2caeb7f0..70c58d83 100644 --- a/Scripts/RemediationScripts/Remediate-InvalidAADObjectRoleAssignments.ps1 +++ b/Scripts/RemediationScripts/Remediate-InvalidAADObjectRoleAssignments.ps1 @@ -51,54 +51,119 @@ function Pre_requisites This command would check pre requisites modules. .DESCRIPTION This command would check pre requisites modules to perform remediation. - #> - - Write-Host "Required modules are: Az.Resources, AzureAD, Az.Accounts, Az.ResourceGraph" -ForegroundColor Cyan - Write-Host "Checking for required modules..." - $availableModules = $(Get-Module -ListAvailable Az.Resources, AzureAD, Az.Accounts, Az.ResourceGraph) - - # Checking if 'Az.Accounts' module is available or not. - if($availableModules.Name -notcontains 'Az.Accounts') - { - Write-Host "Installing module Az.Accounts..." -ForegroundColor Yellow - Install-Module -Name Az.Accounts -Scope CurrentUser -Repository 'PSGallery' - } - else + #> + $folderPaths = $env:PSModulePath -split ';' | Where-Object {$_ -like "*SAWPSModulePath"} + if($folderPaths.Count -gt 0) { - Write-Host "Az.Accounts module is available." -ForegroundColor Green - } + $folderPaths[0] + Write-Host "Detected SAW environment. Installing required modules in SAW module path, if not present." -ForegroundColor Cyan + Write-Host "Required modules are: Az.Resources, Microsoft.Graph, Az.Accounts, Az.ResourceGraph" -ForegroundColor Cyan + Write-Host "Checking for required modules..." + Import-Module PowerShellGet + $availableModules = $(Get-Module -ListAvailable "Az.Resources", "Microsoft.Graph", "Az.Accounts", "Az.ResourceGraph") + + # Checking if 'Az.Accounts' module is available or not. + if($availableModules.Name -notcontains 'Az.Accounts') + { + Write-Host "Installing module Az.Accounts..." -ForegroundColor Yellow + Save-Module -Name Az.Accounts -RequiredVersion 5.3.2 -Path $folderPath + } + else + { + Write-Host "Az.Accounts module is available." -ForegroundColor Green + } - # Checking if 'Az.Resources' module is available or not. - if($availableModules.Name -notcontains 'Az.Resources') - { - Write-Host "Installing module Az.Resources..." -ForegroundColor Yellow - Install-Module -Name Az.Resources -Scope CurrentUser -Repository 'PSGallery' - } - else - { - Write-Host "Az.Resources module is available." -ForegroundColor Green - } + # Checking if 'Az.Resources' module is available or not. + if($availableModules.Name -notcontains 'Az.Resources') + { + Write-Host "Installing module Az.Resources..." -ForegroundColor Yellow + Save-Module -Name Az.Resources -RequiredVersion 9.0.0 -Path $folderPath + } + else + { + Write-Host "Az.Resources module is available." -ForegroundColor Green + } - # Checking if 'ARG' module is available or not. - if($availableModules.Name -notcontains 'Az.ResourceGraph') - { - Write-Host "Installing module Az.ResourceGraph..." -ForegroundColor Yellow - Install-Module -Name Az.ResourceGraph -Scope CurrentUser -Repository 'PSGallery' - } - else - { - Write-Host "Az.ResourceGraph module is available." -ForegroundColor Green - } + # Checking if 'ARG' module is available or not. + if($availableModules.Name -notcontains 'Az.ResourceGraph') + { + Write-Host "Installing module Az.ResourceGraph..." -ForegroundColor Yellow + Save-Module -Name Az.ResourceGraph -RequiredVersion 1.2.1 -Path $folderPath + } + else + { + Write-Host "Az.ResourceGraph module is available." -ForegroundColor Green + } - # Checking if 'AzureAD' module is available or not. - if($availableModules.Name -notcontains 'AzureAD') - { - Write-Host "Installing module AzureAD..." -ForegroundColor Yellow - Install-Module -Name AzureAD -Scope CurrentUser -Repository 'PSGallery' + # Checking if 'Microsoft.Graph' module is available or not. + if($availableModules.Name -notcontains 'Microsoft.Graph') + { + Write-Host "Installing module Microsoft.Graph...`nThis module may take more time to install. Please wait patiently." -ForegroundColor Yellow + Save-Module -Name Microsoft.Graph -RequiredVersion 2.32.0 -Path $folderPath + } + else + { + Write-Host "Microsoft.Graph module is available." -ForegroundColor Green + } + Write-Host "Importing required modules...`nIf you started new PowerShell session, It may take more time to import all modules." -ForegroundColor Cyan + Import-Module Az.Resources + Write-Host "Az.Resources module imported successfully." -ForegroundColor Green + Import-Module Az.Accounts + Write-Host "Az.Accounts module imported successfully." -ForegroundColor Green + Import-Module Az.ResourceGraph + Write-Host "Az.ResourceGraph module imported successfully." -ForegroundColor Green + Import-Module Microsoft.Graph + Write-Host "Microsoft.Graph module imported successfully." -ForegroundColor Green } else { - Write-Host "AzureAD module is available." -ForegroundColor Green + Write-Host "Required modules are: Az.Resources, Microsoft.Graph, Az.Accounts, Az.ResourceGraph" -ForegroundColor Cyan + Write-Host "Checking for required modules..." + $availableModules = $(Get-Module -ListAvailable Az.Resources, Microsoft.Graph, Az.Accounts, Az.ResourceGraph) + + # Checking if 'Az.Accounts' module is available or not. + if($availableModules.Name -notcontains 'Az.Accounts') + { + Write-Host "Installing module Az.Accounts..." -ForegroundColor Yellow + Install-Module -Name Az.Accounts -Scope CurrentUser -Repository 'PSGallery' + } + else + { + Write-Host "Az.Accounts module is available." -ForegroundColor Green + } + + # Checking if 'Az.Resources' module is available or not. + if($availableModules.Name -notcontains 'Az.Resources') + { + Write-Host "Installing module Az.Resources..." -ForegroundColor Yellow + Install-Module -Name Az.Resources -Scope CurrentUser -Repository 'PSGallery' + } + else + { + Write-Host "Az.Resources module is available." -ForegroundColor Green + } + + # Checking if 'ARG' module is available or not. + if($availableModules.Name -notcontains 'Az.ResourceGraph') + { + Write-Host "Installing module Az.ResourceGraph..." -ForegroundColor Yellow + Install-Module -Name Az.ResourceGraph -Scope CurrentUser -Repository 'PSGallery' + } + else + { + Write-Host "Az.ResourceGraph module is available." -ForegroundColor Green + } + + # Checking if 'Microsoft.Graph' module is available or not. + if($availableModules.Name -notcontains 'Microsoft.Graph') + { + Write-Host "Installing module Microsoft.Graph..." -ForegroundColor Yellow + Install-Module -Name Microsoft.Graph -Scope CurrentUser -Repository 'PSGallery' + } + else + { + Write-Host "Microsoft.Graph module is available." -ForegroundColor Green + } } } @@ -167,7 +232,7 @@ function Remove-AzTSInvalidAADAccounts if ([string]::IsNullOrEmpty($isContextSet)) { Write-Host "Connecting to AzAccount..." - Connect-AzAccount -ErrorAction Stop + Connect-AzAccount -Subscription $SubscriptionId -ErrorAction Stop Write-Host "Connected to AzAccount" -ForegroundColor Green } @@ -197,28 +262,28 @@ function Remove-AzTSInvalidAADAccounts $currentLoginUserObjectId = ""; $requiredRoleDefinitionName = @("Owner", "User Access Administrator") - if(($currentLoginRoleAssignments | Where { $_.RoleDefinitionName -in $requiredRoleDefinitionName} | Measure-Object).Count -le 0 ) + if(($currentLoginRoleAssignments | Where { $_.RoleDefinitionName -in $requiredRoleDefinitionName} | Measure-Object).Count -le 0) { # The user does not have direct access to the subscription, checking if the user has access through groups # Need to connect to Azure AD before running any other command for fetching Entra Id related information (e.g. - group membership) try { - Get-AzureADTenantDetail | Out-Null + Connect-MgGraph -TenantId $currentSub.Tenant.Id | Out-Null } catch { - Connect-AzureAD -TenantId $currentSub.Tenant.Id | Out-Null + Write-Host "Failed to connect with Microsoft.Graph" } $allRoleAssignments = Get-AzRoleAssignment -Scope "/subscriptions/$($SubscriptionId)" # Fetch all the role assignmenets for the given scope - $userMemberGroups = Get-AzureADUserMembership -ObjectId $currentSub.Account.Id -All $true | Select-Object -ExpandProperty ObjectId # Fetch all the groups the user has access to and get all the object ids + $userMemberGroups = Get-MgUserMemberOf -UserId $currentSub.Account.Id -All | Select-Object -ExpandProperty ObjectId # Fetch all the groups the user has access to and get all the object ids if(($allRoleAssignments | Where-Object { $_.RoleDefinitionName -in $requiredRoleDefinitionName -and $_.ObjectId -in $userMemberGroups } | Measure-Object).Count -le 0) { Write-Host "Warning: This script can only be run by an [$($requiredRoleDefinitionName -join ", ")]." -ForegroundColor Yellow return; } - $currentLoginUserObjectId = Get-AzureADUser -Filter "userPrincipalName eq '$($currentSub.Account.Id)'" | Select-Object ObjectId -ExpandProperty ObjectId # Fetch the user object id + $currentLoginUserObjectId = Get-MgUser -Filter "userPrincipalName eq '$($currentSub.Account.Id)'" | Select-Object ObjectId -ExpandProperty ObjectId # Fetch the user object id } Write-Host "Current user [$($currentSub.Account.Id)] has the required permission for subscription [$($SubscriptionId)]." -ForegroundColor Green @@ -244,8 +309,7 @@ function Remove-AzTSInvalidAADAccounts if(($ObjectIds | Measure-Object).Count -eq 0) { # Getting all classic role assignments. - $classicAssignments = [ClassicRoleAssignments]::new() - $res = $classicAssignments.GetClassicRoleAssignments($subscriptionId) + $res = GetClassicRoleAssignments -SubscriptionId $subscriptionId $classicDistinctRoleAssignmentList = $res.value | Where-Object { ![string]::IsNullOrWhiteSpace($_.properties.emailAddress) } # Renaming property name @@ -262,14 +326,8 @@ function Remove-AzTSInvalidAADAccounts $currentRoleAssignmentList = $currentRoleAssignmentList | Where-Object {![string]::IsNullOrWhiteSpace($_.ObjectId)}; $currentRoleAssignmentList | select -Unique -Property 'ObjectId' | ForEach-Object { $distinctObjectIds += $_.ObjectId } - - $method = [Microsoft.PowerShell.Commands.WebRequestMethod]::Get - $classicAssignments = [ClassicRoleAssignments]::new() - $headers = $classicAssignments.GetAuthHeader() - # Getting MDC reported deprecated account object ids. - $mdcDeprecated = [MDCDeprecatedAccounts]::new() - $mdcDeprecatedAccountList = $mdcDeprecated.GetMDCDeprecatedAccounts([string] $SubscriptionId) + $mdcDeprecatedAccountList = GetMDCDeprecatedAccounts -SubcriptionId $SubscriptionId $mdcDeprecatedRoleAssignmentList = @(); if (($mdcDeprecatedAccountList | Measure-Object).Count -gt 0) @@ -300,13 +358,12 @@ function Remove-AzTSInvalidAADAccounts # Connect to Azure Active Directory. try { - # Check if Connect-AzureAD session is already active - Get-AzureADUser -ObjectId $currentLoginUserObjectId | Out-Null + # Connect with Microsoft Graph + Connect-MgGraph -TenantId $currentSub.Tenant.Id -ErrorAction Stop } catch { - Write-Host "Connecting to Azure AD..." - Connect-AzureAD -TenantId $currentSub.Tenant.Id -ErrorAction Stop + Write-Host "Failed to connect with Microsoft Graph..." } # Batching object ids in count of 900. @@ -325,7 +382,7 @@ function Remove-AzTSInvalidAADAccounts $subRange = $distinctObjectIds[$i..$endRange] # Getting active identities from Azure Active Directory. - $subActiveIdentities = Get-AzureADObjectByObjectId -ObjectIds $subRange + $subActiveIdentities = Get-MgDirectoryObjectById -Ids $subRange # Safe Check if(($subActiveIdentities | Measure-Object).Count -le 0) { @@ -334,7 +391,7 @@ function Remove-AzTSInvalidAADAccounts return; } - $activeIdentities += $subActiveIdentities.ObjectId + $activeIdentities += $subActiveIdentities | Select-Object -ExpandProperty Id } $invalidAADObjectIds = $distinctObjectIds | Where-Object { $_ -notin $activeIdentities} @@ -345,7 +402,7 @@ function Remove-AzTSInvalidAADAccounts if(($classicRoleAssignments | Measure-Object).count -gt 0) { $classicRoleAssignments | ForEach-Object { - $userDetails = Get-AzureADUser -Filter "userPrincipalName eq '$($_.SignInName)' or Mail eq '$($_.SignInName)'" + $userDetails = Get-MgUser -Filter "userPrincipalName eq '$($_.SignInName)' or Mail eq '$($_.SignInName)'" if (($userDetails | Measure-Object).Count -eq 0 ) { $invalidClassicRoles += $_ @@ -367,13 +424,13 @@ function Remove-AzTSInvalidAADAccounts # Connect to Azure Active Directory. try { - # Check if Connect-AzureAD session is already active - Get-AzureADUser -ObjectId $currentLoginUserObjectId | Out-Null + # Connect with Microsoft Graph + Connect-MgGraph -Scopes "User.Read.All", "Group.ReadWrite.All" -ErrorAction Stop } catch { - Write-Host "Connecting to Azure AD..." - Connect-AzureAD -ErrorAction Stop + Write-Host "Failed to connect with Microsoft Graph..." + } $allRoleAssignments = Import-Csv -LiteralPath $FilePath @@ -420,8 +477,9 @@ function Remove-AzTSInvalidAADAccounts { Write-Host "Found [$($invalidClassicRolesCount)] invalid classic role assignments for the subscription [$($SubscriptionId)]" -ForegroundColor Cyan } - - $folderPath = [Environment]::GetFolderPath("MyDocuments") + $folderPath = ($env:PSModulePath -split ';' | + Where-Object { $_ -match '\\Documents(\\|$)' } | + Select-Object -First 1) -replace '(?i)^(.*?\\Documents)(?:\\.*)?$','$1' if (Test-Path -Path $folderPath) { $folderPath += "\AzTS\Remediation\Subscriptions\$($subscriptionid.replace("-","_"))\$((Get-Date).ToString('yyyyMMdd_hhmm'))\InvalidAADAccounts\" @@ -498,8 +556,7 @@ function Remove-AzTSInvalidAADAccounts $isServiceAdminAccount = $true; } - $classicAssignments = [ClassicRoleAssignments]::new() - $res = $classicAssignments.DeleteClassicRoleAssignment($_.RoleAssignmentId, $isServiceAdminAccount) + $res = DeleteClassicRoleAssignment -roleAssignmentId $_.RoleAssignmentId -isServiceAdminAccount $isServiceAdminAccount if(($null -ne $res) -and ($res.StatusCode -eq 202 -or $res.StatusCode -eq 200)) { @@ -509,7 +566,6 @@ function Remove-AzTSInvalidAADAccounts } catch { - $isRemoved = $false Write-Host "Not able to remove invalid classic role assignment. ErrorMessage [$($_)]" -ForegroundColor Red } } @@ -556,7 +612,7 @@ function Get-ARGData $graphResult = Search-AzGraph -Query $kqlQuery -First $batchSize } - $kqlResponse += $graphResult.data.ToArray() + $kqlResponse += $graphResult.data | Select-Object * if ($graphResult.data.Count -lt $batchSize) { break; @@ -566,112 +622,85 @@ function Get-ARGData return $kqlResponse } - - -class ClassicRoleAssignments +function GetClassicRoleAssignments { - [PSObject] GetAuthHeader() + param ( + [string] + $SubscriptionId + ) + $content = $null + try { - [psobject] $headers = $null - try - { - $resourceAppIdUri = "https://management.core.windows.net/" - $rmContext = Get-AzContext - $authResult = [Microsoft.Azure.Commands.Common.Authentication.AzureSession]::Instance.AuthenticationFactory.Authenticate( - $rmContext.Account, - $rmContext.Environment, - $rmContext.Tenant, - [System.Security.SecureString] $null, - "Never", - $null, - $resourceAppIdUri); - - $header = "Bearer " + $authResult.AccessToken - $headers = @{"Authorization"=$header;"Content-Type"="application/json";} - } - catch - { - Write-Host "Error occurred while fetching auth header. ErrorMessage [$($_)]" -ForegroundColor Red - } - return($headers) + $accessToken = (Get-AzAccessToken -ResourceUrl "https://management.azure.com" -AsSecureString).Token + $armUri = "https://management.azure.com/subscriptions/$($subscriptionId)/providers/Microsoft.Authorization/classicadministrators?api-version=2015-06-01" + # API to get classic role assignments + $response = Invoke-RestMethod -Method Get -Uri $armUri -SkipHeaderValidation -Authentication Bearer -Token (Get-AzAccessToken -ResourceUrl "https://management.azure.com" -AsSecureString).Token -UseBasicParsing + $content = $response.value } - - [PSObject] GetClassicRoleAssignments([string] $subscriptionId) + catch { - $content = $null - try - { - $armUri = "https://management.azure.com/subscriptions/$($subscriptionId)/providers/Microsoft.Authorization/classicadministrators?api-version=2015-06-01" - $headers = $this.GetAuthHeader() - # API to get classic role assignments - $response = Invoke-WebRequest -Method Get -Uri $armUri -Headers $headers -UseBasicParsing - $content = ConvertFrom-Json $response.Content - } - catch - { - Write-Host "Error occurred while fetching classic role assignment. ErrorMessage [$($_)]" -ForegroundColor Red - } - - return($content) + Write-Host "Error occurred while fetching classic role assignment. ErrorMessage [$($_)]" -ForegroundColor Red } + + return $content +} +function DeleteClassicRoleAssignment +{ + param ( + [string] + $roleAssignmentId, - [PSObject] DeleteClassicRoleAssignment([string] $roleAssignmentId, [bool] $isServiceAdminAccount) + [bool] + $isServiceAdminAccount + ) + $content = $null + try { - $content = $null - try + $armUri = "https://management.azure.com" + $roleAssignmentId + "?api-version=2015-06-01" + if ($isServiceAdminAccount) { - $armUri = "https://management.azure.com" + $roleAssignmentId + "?api-version=2015-06-01" - if ($isServiceAdminAccount) - { - $armUri += "&adminType=serviceAdmin" - } - $headers = $this.GetAuthHeader() - - # API to get classic role assignments - $response = Invoke-WebRequest -Method Delete -Uri $armUri -Headers $headers -UseBasicParsing - $content = $response - } - catch - { - Write-Host "Error occurred while deleting classic role assignment. ErrorMessage [$($_)]" -ForegroundColor Red - throw; + $armUri += "&adminType=serviceAdmin" } - return($content) + # API to get classic role assignments + $accessToken = (Get-AzAccessToken -ResourceUrl "https://management.azure.com" -AsSecureString).Token + $response = Invoke-RestMethod -Method Delete -Uri $armUri -SkipHeaderValidation -Authentication Bearer -Token $accessToken -UseBasicParsing + $content = $response.value } + catch + { + Write-Host "Error occurred while deleting classic role assignment. ErrorMessage [$($_)]" -ForegroundColor Red + throw; + } + + return $content } -class MDCDeprecatedAccounts +function GetMDCDeprecatedAccounts { - [PSObject] GetMDCDeprecatedAccounts([string] $SubcriptionId) + param ( + [string] + $SubcriptionId + ) + $response = @() + $invalidObjectIds = @() + $response += Get-ARGData -kqlQuery "securityresources | where type == 'microsoft.security/assessments' | where name =~ '1ff0b4c9-ed56-4de6-be9c-d7ab39645926' and subscriptionId =~ '$($SubcriptionId)'" + if (($response | Measure-Object).Count -gt 0 ) { - $response = @() - $invalidObjectIds = @() - - $response += Get-ARGData -kqlQuery "securityresources | where type == 'microsoft.security/assessments' | where name =~ '1ff0b4c9-ed56-4de6-be9c-d7ab39645926' and subscriptionId =~ '$($SubcriptionId)'" - - if (($response | Measure-Object).Count -gt 0 ) + $mdcAssessmentState = $response[0].properties.status.code + if ((-not [string]::IsNullOrWhiteSpace($mdcAssessmentState)) -and ($mdcAssessmentState -eq 'Unhealthy') -and (-not [string]::IsNullOrWhiteSpace($response[0].properties.additionalData.subAssessmentsLink))) + { + $nextSubAssessmentLink = $response[0].properties.additionalData.subAssessmentsLink + $SubAssessmentResponse = Get-ARGData -kqlQuery "securityresources | where type == 'microsoft.security/assessments/subassessments' | where id contains '$($nextSubAssessmentLink)'" + if (($SubAssessmentResponse | Measure-Object).Count -gt 0 ) { - $mdcAssessmentState = $response[0].properties.status.code - - if ((-not [string]::IsNullOrWhiteSpace($mdcAssessmentState)) -and ($mdcAssessmentState -eq 'Unhealthy') -and (-not [string]::IsNullOrWhiteSpace($response[0].properties.additionalData.subAssessmentsLink))) - { - - $nextSubAssessmentLink = $response[0].properties.additionalData.subAssessmentsLink - - $SubAssessmentResponse = Get-ARGData -kqlQuery "securityresources | where type == 'microsoft.security/assessments/subassessments' | where id contains '$($nextSubAssessmentLink)'" - - if (($SubAssessmentResponse | Measure-Object).Count -gt 0 ) - { - $invalidObjectIds += foreach ($obj in $SubAssessmentResponse) { ($obj.properties.resourceDetails.id -split "/")[-1] } - } - } - + $invalidObjectIds += foreach ($obj in $SubAssessmentResponse) { ($obj.properties.resourceDetails.id -split "/")[-1] } } + } - return($invalidObjectIds) - } + return $invalidObjectIds + } # ***************************************************** #