azure-ad-recovery-manager.functions.ps1
function GetUsersAndGroups { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseUsingScopeModifierInNewRunspaces', '', Justification = 'Using ArgumentList')] [CmdletBinding()] param ( [switch] $Incremental, [switch] $AsJob ) begin { # Initialize function variables $WarningPreference = 'SilentlyContinue' $functionName = $MyInvocation.MyCommand.Name $usersAndGroups = @() SetNumberOfJobs Write-Verbose "[$(Get-Date -Format s)] : $functionName : Begin function.." } process { try { Write-Verbose "[$(Get-Date -Format s)] : $functionName : Determining users and groups.." $users = Get-AzADUser $groups = Get-AzADGroup # $groups = $groups | Where-Object { !($_.MailEnabled) } if ($Incremental.IsPresent) { $backupUsers = Find-User -All $backupGroups = Find-Group -All $usersObject = Compare-Object -ReferenceObject $users.Id -DifferenceObject $backupUsers.Id | Select-Object -ExpandProperty InputObject $groupsObject = Compare-Object -ReferenceObject $groups.Id -DifferenceObject $backupGroups.Id | Select-Object -ExpandProperty InputObject $groupsToAdd = @() $usersToAdd = @() if ($usersObject) { foreach ($obj in $usersObject) { $usersToAdd += $users | Where-Object { $_.Id -eq $obj } } } if ($groupsObject) { foreach ($obj in $groupsObject) { $groupsToAdd += $groups | Where-Object { $_.Id -eq $obj } } } $backupOutput = [BackupOutput]@{ Users = $usersToAdd Groups = $groupsToAdd } } else { $backupOutput = [BackupOutput]@{ Users = $users Groups = $groups } } if ($AsJob.IsPresent) { $path = "$($PWD.Path)\Job-Results" if (-not (Test-Path $path)) { $exportPath = New-Item -Path $path -ItemType Directory | Select-Object -ExpandProperty FullName } else { $exportPath = $path } # $numberOfJobs = Get-CimInstance -ClassName Win32_Processor | Select-Object -ExpandProperty NumberOfLogicalProcessors $numberOfJobs = $env:AZURE_AD_BACKUP_JOBS_COUNT if ($backupOutput.Groups.Count -le $numberOfJobs) { $numberOfJobs = 1 } $groupsPerBatch = [System.Math]::Round($backupOutput.Groups.Count / $numberOfJobs) $totalGroups = $backupOutput.Groups.Count $counter = 0 $batchNameCounter = 0 $batches = $groupsPerBatch while ($counter -lt $totalGroups) { $groupsSet = $backupOutput.Groups[$counter..$groupsPerBatch] $job = Start-Job -Name "Batch-$batchNameCounter" -ScriptBlock { param([object[]] $Groups, [string] $FilePath) $result = @() foreach ($group in $Groups) { Write-Verbose "[$(Get-Date -Format s)] : $functionName : Working with [$($group.DisplayName)].." $members = Get-AzADGroupMember -GroupObjectId $group.Id if ($members) { Write-Verbose "[$(Get-Date -Format s)] : $functionName : Retrieving members from [$($group.DisplayName)].." Write-Verbose "[$(Get-Date -Format s)] : $functionName : Found [$($members.Count)] users in [$($group.DisplayName)].." $memberList = @() $h = [PSCustomObject]@{ GroupName = $group.DisplayName GroupId = $group.Id } $members | ForEach-Object { $memberList += [PSCustomObject]@{ DeletedDateTime = $_.DeletedDateTime Name = $_.DisplayName Id = $_.Id OdataType = $_.OdataType } } Add-Member -InputObject $h -MemberType NoteProperty -Name Users -Value $memberList -TypeName PSCustomObject -Force $result += $h $result | ConvertTo-Json -Depth 99 | Set-Content -Path $FilePath -Encoding utf8 } } } -ArgumentList @($groupsSet, "$exportPath\Batch-$batchNameCounter.json") Write-Verbose "[$(Get-Date -Format s)] : $functionName : Initiated job in background [$($job.Id) - $($job.Name)].." $counter = $groupsPerBatch $groupsPerBatch = $counter + $batches $batchNameCounter++ } $null = Get-Job | Where-Object { $_.Name -match 'Batch' } | Wait-Job Write-Verbose "[$(Get-Date -Format s)] : $functionName : Merging Json files.." $files = Get-ChildItem -Path $exportPath -Filter "*.json" | Select-Object -ExpandProperty FullName foreach ($file in $files) { Write-Verbose "[$(Get-Date -Format s)] : $functionName : Working with [$(Split-Path -Path $file -Leaf)].." $results += Get-Content -Path $file | ConvertFrom-Json } $results | ConvertTo-Json -Depth 99 | Out-File -FilePath "$exportPath\UsersAndGroups.json" -Encoding utf8 $res = Get-Content -Path "$exportPath\UsersAndGroups.json" -Raw | ConvertFrom-Json Add-Member -InputObject $backupOutput -MemberType NoteProperty -Name UsersAndGroups -Value $res -TypeName PSCustomObject -Force return $backupOutput } else { foreach ($group in $backupOutput.Groups) { Write-Verbose "[$(Get-Date -Format s)] : $functionName : Working with [$($group.DisplayName)].." $members = Get-AzADGroupMember -GroupObjectId $group.Id if ($members) { Write-Verbose "[$(Get-Date -Format s)] : $functionName : Retrieving members from [$($group.DisplayName)].." Write-Verbose "[$(Get-Date -Format s)] : $functionName : Found [$($members.Count)] users in [$($group.DisplayName)].." $memberList = @() $h = [PSCustomObject]@{ GroupName = $group.DisplayName GroupId = $group.Id } $members | ForEach-Object { $memberList += [PSCustomObject]@{ DeletedDateTime = $_.DeletedDateTime Name = $_.DisplayName Id = $_.Id OdataType = $_.OdataType } } Add-Member -InputObject $h -MemberType NoteProperty -Name Users -Value $memberList -TypeName PSCustomObject -Force $usersAndGroups += $h } } Add-Member -InputObject $backupOutput -MemberType NoteProperty -Name UsersAndGroups -Value $usersAndGroups -TypeName PSCustomObject -Force return $backupOutput } } catch { if ($_.Exception.Message.Contains('DifferenceObject')) { Write-Error "An Error Occurred at line $($_.InvocationInfo.ScriptLineNumber). Incremental Operation is supported only to add new groups. Check if database exists and not empty." } else { Write-Error "An Error Occurred at line $($_.InvocationInfo.ScriptLineNumber). Message: $($_.Exception.Message)." } } } end { Write-Verbose "[$(Get-Date -Format s)] : $functionName : End function.." # clean up Get-Job | Where-Object { $_.Name -match 'Batch' } | Remove-Job -ErrorAction SilentlyContinue if ((![string]::IsNullOrEmpty($exportPath)) -and (Test-Path $exportPath)) { Remove-Item $exportPath -Recurse -Force } } } function CreateDatabase { $dbPath = (GetDatabasePath) if (-not (Test-Path $dbPath)) { $database = New-Item -Path $dbPath -ItemType File | Select-Object -ExpandProperty FullName } else { $database = $dbPath } return $database } function GetDatabasePath { if (!([string]::IsNullOrEmpty($env:AZURE_AD_BACKUP_DATABASE))) { return $env:AZURE_AD_BACKUP_DATABASE } else { throw "Please set the backup path first by running 'Set-BackupPath' cmdlet." } } function DropTable([string] $TableName) { Invoke-SqliteQuery -DataSource (GetDatabasePath) -Query "DROP TABLE IF EXISTS $TableName" } function CreateTable([string] $TableName, [string[]] $Columns) { $table = [System.Text.StringBuilder]::new("CREATE TABLE IF NOT EXISTS $TableName (") $null = $Columns | ForEach-Object { if ($_ -eq $Columns[-1]) { $table.Append("$_") } else { $table.Append("$_, ") } } $tableToCreate = $table.Append(")").ToString() Invoke-SqliteQuery -DataSource (GetDatabasePath) -Query "$tableToCreate" } function GetConfigFile { return (Get-ChildItem -Filter "config.json" | Select-Object -ExpandProperty FullName) } function Query([string] $TableName, [string] $Condition) { return Invoke-SqliteQuery -DataSource (GetDatabasePath) -Query "SELECT * FROM $TableName $Condition" -ErrorAction SilentlyContinue } function IsGroupExists([string] $GroupId) { return ([bool] (Get-AzADGroup -ObjectId $GroupId -ErrorAction SilentlyContinue)) } function SetNumberOfJobs([int] $NumberOfJobs) { if ($NumberOfJobs -le 0) { $NumberOfJobs = 10 } [System.Environment]::SetEnvironmentVariable('AZURE_AD_BACKUP_JOBS_COUNT', $NumberOfJobs, [System.EnvironmentVariableTarget]::Process) } function ValidateLogin() { try { Get-AzTenant -ErrorAction Stop | Out-Null } catch { if ($_.Exception.Message.Contains('is not recognized as a name of a cmdlet')) { throw "An error occurred: Couldn't find the Azure module 'Az' in the current session. Please install the module or import it if already installed and try again." } if ($_.Exception.Message.Contains('Run Connect-AzAccount to login.')) { throw "An error occurred: Please login to your azure tenant to perform the backup or restore operation. Run Connect-AzAccount -TenantId <TenantId> to login." } } } function Backup-AzADSecurityGroup { [CmdletBinding(HelpUri = "https://github.com/hkarthik7/azure-ad-recovery-manager/blob/main/src/docs/Backup-AzADSecurityGroup.md")] param ( [switch] $AsJob, [switch] $Incremental, [switch] $ShowOutput, [ValidateRange(1,20)] [int] $NumberOfJobs ) begin { ValidateLogin $functionName = $MyInvocation.MyCommand.Name SetNumberOfJobs $NumberOfJobs Write-Verbose "[$(Get-Date -Format s)] : $functionName : Begin function.." $schema = [Schema]@{ Tables = @( [Table]@{ TableName = 'users' Columns = @( "id VARCHAR(50) PRIMARY KEY", "displayname TEXT", "mail TEXT", "odatatype TEXT", "userprincipalname TEXT" ) }, [Table]@{ TableName = 'groups' Columns = @( "id VARCHAR(50) PRIMARY KEY", "displayname TEXT", "description TEXT", "mailnickname TEXT", "mailenabled INTEGER", "createddatetime TEXT", "isassignabletorole INTEGER", "owner TEXT", "reneweddatetime TEXT", "securityenabled INTEGER", "securityidentifier TEXT" ) }, [Table]@{ TableName = 'usersandgroups' Columns = @( "groupid VARCHAR(50)", "displayname TEXT", "odatatype TEXT", "userid VARCHAR(50) REFERENCES users(id)", "PRIMARY KEY (groupid, userid)" ) }, [Table]@{ TableName = 'roleassignments' Columns = @( "roleassignmentName TEXT", "roleassignmentId TEXT VARCHAR(50) PRIMARY KEY", "scope TEXT", "displayname TEXT", "signinname TEXT", "roledefinitionname TEXT", "roledefinitionid TEXT", "objectid TEXT", "objecttype TEXT", "candelegate TEXT", "description TEXT", "conditionversion TEXT", "condition TEXT" ) } ) } } process { try { if ((GetDatabasePath)) { if ($Incremental.IsPresent) { if ($AsJob.IsPresent) { $backupOutput = GetUsersAndGroups -AsJob -Incremental } else { $backupOutput = GetUsersAndGroups -Incremental } } else { if ($AsJob.IsPresent) { $backupOutput = GetUsersAndGroups -AsJob } else { $backupOutput = GetUsersAndGroups } } # 1) Create database $database = CreateDatabase # 2) Create tables (users, groups and usersandgroups) foreach ($table in $schema.Tables) { if (!$Incremental.IsPresent) { DropTable -TableName $table.TableName } CreateTable -TableName $table.TableName -Columns $table.Columns if ($table.TableName -eq 'users') { # insert data if ($backupOutput.Users) { $usersDataTable = $backupOutput.Users | ForEach-Object { [User]@{ Id = $_.Id DisplayName = $_.DisplayName Mail = if (!([string]::IsNullOrEmpty($_.Mail))) { $_.Mail } else { $null } UserPrincipalName = if (!([string]::IsNullOrEmpty($_.UserPrincipalName))) { $_.UserPrincipalName } else { $null } OdataType = $_.OdataType } } | Out-DataTable Invoke-SqliteBulkCopy -DataTable $usersDataTable -DataSource $database -Table $table.TableName -ConflictClause Ignore -Force } } if ($table.TableName -eq 'groups') { if ($backupOutput.Groups) { $groupsDataTable = $backupOutput.Groups | ForEach-Object { [Group]@{ Id = $_.Id DisplayName = $_.DisplayName MailNickname = $_.MailNickname Description = $_.Description MailEnabled = $_.MailEnabled CreatedDateTime = if (!([string]::IsNullOrEmpty($_.CreatedDateTime))) { (Get-Date $_.CreatedDateTime -Format s) } else { $null } IsAssignableToRole = $_.IsAssignableToRole Owner = $_.Owner RenewedDateTime = if (!([string]::IsNullOrEmpty($_.RenewedDateTime))) { (Get-Date $_.RenewedDateTime -Format s) } else { $null } SecurityEnabled = $_.SecurityEnabled SecurityIdentifier = $_.SecurityIdentifier } } | Out-DataTable Invoke-SqliteBulkCopy -DataTable $groupsDataTable -DataSource $database -Table $table.TableName -ConflictClause Ignore -Force } } if ($table.TableName -eq 'roleassignments') { $roleAssignments = Get-AzRoleAssignment | Out-DataTable Invoke-SqliteBulkCopy -DataTable $roleAssignments -DataSource $database -Table $table.TableName -ConflictClause Ignore -Force } if ($table.TableName -eq 'usersandgroups') { $results = Query -TableName $table.TableName if ($results) { $backupOutput.UsersAndGroups = $backupOutput.UsersAndGroups | ForEach-Object { if ($_.GroupId -notin $results.groupid) { $_ } } } if ($backupOutput.UsersAndGroups) { $relationship = $backupOutput.UsersAndGroups | ForEach-Object { [UserAndGroup]@{ GroupId = $_.GroupId DisplayName = $_.GroupName OdataType = @($_.Users.OdataType) UserId = @($_.Users.Id) } } # A group can contain security group(s), device(s), spn(s) and user(s). # Adding the group memebers id to users table to form many to many relationship. $usersTable = $schema.Tables | Where-Object { $_.TableName -eq 'users' } $userTableQueryBuilder = [System.Text.StringBuilder]::new() $null = $userTableQueryBuilder.AppendLine("INSERT OR IGNORE INTO $($usersTable.TableName) (id, odatatype, displayname) ") $null = $userTableQueryBuilder.AppendLine("VALUES ") foreach ($entry in $backupOutput.UsersAndGroups) { foreach ($user in $entry.Users) { if ($user.Name.Contains("'")) { $null = $userTableQueryBuilder.AppendLine("('$($user.Id)', '$($user.OdataType)', '$($user.Name.Replace("'", "''"))'), ") } else { $null = $userTableQueryBuilder.AppendLine("('$($user.Id)', '$($user.OdataType)', '$($user.Name)'), ") } } } Invoke-SqliteQuery -DataSource $database -Query ($userTableQueryBuilder.ToString().Trim().TrimEnd(",")) $queryBuilder = [System.Text.StringBuilder]::new() $null = $queryBuilder.AppendLine("INSERT OR IGNORE INTO $($table.TableName) (userid, groupid, odatatype, displayname) ") $null = $queryBuilder.AppendLine("VALUES ") foreach ($value in $relationship) { for ($i = 0; $i -lt $value.UserId.Count; $i++) { $null = $queryBuilder.AppendLine("((SELECT id FROM users WHERE id = '$($value.UserId[$i])'), '$($value.groupid)', '$($value.odatatype[$i])', '$($value.displayname)'), ") } } Invoke-SqliteQuery -DataSource $database -Query ($queryBuilder.ToString().Trim().TrimEnd(",")) } } } $report = [BackupReport]@{ ScannedDateTime = Get-Date NumberOfUsersScanned = $backupOutput.Users.Count NumberOfGroupsScanned = $backupOutput.Groups.Count NumberOfGroupMembersScanned = ($backupOutput.UsersAndGroups.Users.Id | Select-Object -Unique).Count NumberOfRoleAssignmentsScanned = $roleAssignments.Rows.Count } Write-Verbose "[$(Get-Date -Format s)] : $functionName : Generating backup report.." $report | Export-Csv -Path "$(Split-Path -Path (GetDatabasePath) -Parent)\Azure-AD-Backup-Report.csv" -Encoding utf8 -Force -NoTypeInformation -Append if ($ShowOutput.IsPresent) { return $backupOutput } } else { throw "Database path is empty. Run 'Set-BackupPath' cmdlet and set the backup path." } } catch { Write-Error "An Error Occurred at line $($_.InvocationInfo.ScriptLineNumber). Message: $($_.Exception.Message)." } } end { Write-Verbose "[$(Get-Date -Format s)] : $functionName : End function.." } } function Find-Group { [CmdletBinding(DefaultParameterSetName = "ByPattern", HelpUri = "https://github.com/hkarthik7/azure-ad-recovery-manager/blob/main/src/docs/Find-Group.md")] param ( [Parameter(Mandatory, ParameterSetName = "ByName")] [ValidateNotNullOrEmpty()] [string] $Name, [Parameter(Mandatory, ParameterSetName = "ByPattern", ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string] $NamePattern, [Parameter(Mandatory, ParameterSetName = "ById")] [ValidateNotNullOrEmpty()] [string] $Id, [Parameter(Mandatory, ParameterSetName = "All")] [switch] $All ) process { try { if ((GetDatabasePath)) { $table = 'groups' if ($PSCmdlet.ParameterSetName -eq 'ByName') { [Group[]] $res = Query -TableName $table -Condition "WHERE displayname = '$Name'" if ($res) { return $res } Write-Warning "Couldn't find group with name '$Name'" } if ($PSCmdlet.ParameterSetName -eq 'ByPattern') { [Group[]] $res = Query -TableName $table -Condition "WHERE displayname LIKE '$NamePattern%'" if ($res) { return $res } Write-Warning "Couldn't find group with name '$NamePattern'. If the name is valid, try using the pattern like %$NamePattern." } if ($PSCmdlet.ParameterSetName -eq 'ById') { [Group] $res = Query -TableName $table -Condition "WHERE id = '$Id'" if ($res) { return $res } Write-Warning "Couldn't find group with id '$Id'" } if (($PSCmdlet.ParameterSetName -eq 'All') -or ($All.IsPresent)) { return ([group[]] (Query $table)) } } else { throw "Couldn't find the database in provided path. Please run 'Set-BackupPath' cmdlet to set the database path." } } catch { Write-Error "An error occurred: $($_.Exception.Message)." } } } function Find-GroupMemberShip { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '', Justification = 'Output type varies for each return value')] [CmdletBinding(DefaultParameterSetName = "ByPattern", HelpUri = "https://github.com/hkarthik7/azure-ad-recovery-manager/blob/main/src/docs/Find-GroupMemberShip.md")] param ( [Parameter(Mandatory, ParameterSetName = "ByName")] [ValidateNotNullOrEmpty()] [string] $Name, [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = "ByPattern")] [ValidateNotNullOrEmpty()] [string] $NamePattern, [Parameter(Mandatory, ParameterSetName = "ById")] [ValidateNotNullOrEmpty()] [string] $Id ) process { try { if ((GetDatabasePath)) { $table = 'usersandgroups' [GroupMembership[]] $results = @() if ($PSCmdlet.ParameterSetName -eq 'ByName') { $group = Find-Group -Name $Name } if ($PSCmdlet.ParameterSetName -eq 'ByPattern') { $group = Find-Group -NamePattern $NamePattern } if ($PSCmdlet.ParameterSetName -eq 'ById') { $group = Find-Group -Id $Id } if ($group) { $group | ForEach-Object { $result = Query -TableName $table -Condition "WHERE groupid = '$($_.Id)'" if ($result) { [Member[]] $members = @() $groupObject = [PSCustomObject]@{ GroupId = $result.groupid | Select-Object -First 1 GroupName = $result.displayname | Select-Object -First 1 } $result | ForEach-Object { $members += [Member]@{ UserId = $_.userid UserName = Find-User -Id $_.userid | Select-Object -ExpandProperty DisplayName } } Add-Member -InputObject $groupObject -MemberType NoteProperty -Name "Members" -Value $members -TypeName PSCustomObject $results += $groupObject } else { Write-Warning "Couldn't find any results for group: $($_.DisplayName)." } } return $results } } else { throw "Couldn't find the database in provided path. Please run 'Set-BackupPath' cmdlet to set the database path." } } catch { Write-Error "An error occurred: $($_.Exception.Message)." } } } function Find-RoleAssignment { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '', Justification = 'Returning multiple output types and it does not have to be explicitly specified.')] [CmdletBinding(DefaultParameterSetName = "ByPattern", HelpUri = "https://github.com/hkarthik7/azure-ad-recovery-manager/blob/main/src/docs/Find-RoleAssignment.md")] param ( [Parameter(Mandatory, ParameterSetName = "ByName")] [ValidateNotNullOrEmpty()] [string] $Name, [Parameter(Mandatory, ParameterSetName = "ByPattern", ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string] $NamePattern, [Parameter(Mandatory, ParameterSetName = "ById")] [ValidateNotNullOrEmpty()] [string] $Id, [Parameter(Mandatory, ParameterSetName = "All")] [switch] $All ) process { try { if ((GetDatabasePath)) { $table = 'roleassignments' $roleAssignments = @() if ($PSCmdlet.ParameterSetName -eq 'ByName') { [Group[]] $res = Find-Group -Name $Name if ($res) { $res | ForEach-Object { $roleAssignments += Query -TableName $table -Condition "WHERE objectid = '$($_.Id)'" } return $roleAssignments } } if ($PSCmdlet.ParameterSetName -eq 'ByPattern') { [Group[]] $res = Find-Group -NamePattern $NamePattern if ($res) { $res | ForEach-Object { $roleAssignments += Query -TableName $table -Condition "WHERE objectid = '$($_.Id)'" } return $roleAssignments } } if ($PSCmdlet.ParameterSetName -eq 'ById') { [Group] $res = Find-Group -Id $Id if ($res) { $roleAssignments += Query -TableName $table -Condition "WHERE objectid = '$($res.Id)'" return $roleAssignments } } if (($PSCmdlet.ParameterSetName -eq 'All') -or ($All.IsPresent)) { return (Query $table) } } else { throw "Couldn't find the database in provided path. Please run 'Set-BackupPath' cmdlet to set the database path." } } catch { Write-Error "An error occurred: $($_.Exception.Message)." } } } function Find-User { [CmdletBinding(DefaultParameterSetName = "ByPattern", HelpUri = "https://github.com/hkarthik7/azure-ad-recovery-manager/blob/main/src/docs/Find-User.md")] param ( [Parameter(Mandatory, ParameterSetName = "ByName")] [ValidateNotNullOrEmpty()] [string] $Name, [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = "ByPattern")] [ValidateNotNullOrEmpty()] [string] $NamePattern, [Parameter(Mandatory, ParameterSetName = "ById")] [ValidateNotNullOrEmpty()] [string] $Id, [Parameter(Mandatory, ParameterSetName = "ByEmail")] [ValidateNotNullOrEmpty()] [string] $Email, [Parameter(Mandatory, ParameterSetName = "ByUPN")] [ValidateNotNullOrEmpty()] [string] $UserPrincipalName, [Parameter(Mandatory, ParameterSetName = "All")] [switch] $All ) process { try { if ((GetDatabasePath)) { $table = 'users' if ($PSCmdlet.ParameterSetName -eq 'ByName') { [User[]] $res = Query -TableName $table -Condition "WHERE displayname = '$Name'" if ($res) { return $res } Write-Warning "Couldn't find user with name '$Name'" } if ($PSCmdlet.ParameterSetName -eq 'ByPattern') { [User[]] $res = Query -TableName $table -Condition "WHERE displayname LIKE '$NamePattern%'" if ($res) { return $res } Write-Warning "Couldn't find user with name '$NamePattern'. If the name is valid, try using the pattern like %$NamePattern or try running cmdlet with -Email and pass the email id." } if ($PSCmdlet.ParameterSetName -eq 'ById') { [User] $res = Query -TableName $table -Condition "WHERE id = '$Id'" if ($res) { return $res } Write-Warning "Couldn't find user with id '$Id'" } if ($PSCmdlet.ParameterSetName -eq 'ByEmail') { [User] $res = Query -TableName $table -Condition "WHERE mail = '$Email'" if ($res) { return $res } Write-Warning "Couldn't find user with email '$Email'" } if ($PSCmdlet.ParameterSetName -eq 'ByUPN') { [User] $res = Query -TableName $table -Condition "WHERE userprincipalname = '$UserPrincipalName'" if ($res) { return $res } Write-Warning "Couldn't find user with email '$Email'" } if (($PSCmdlet.ParameterSetName -eq 'All') -or ($All.IsPresent)) { return ([User[]] (Query $table)) } } else { throw "Couldn't find the database in provided path. Please run 'Set-BackupPath' cmdlet to set the database path." } } catch { Write-Error "An error occurred: $($_.Exception.Message)." } } } function Find-UserMemberShip { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '', Justification = 'Output type varies for each return value')] [CmdletBinding(DefaultParameterSetName = "ByPattern", HelpUri = "https://github.com/hkarthik7/azure-ad-recovery-manager/blob/main/src/docs/Find-UserMemberShip.md")] param ( [Parameter(Mandatory, ParameterSetName = "ByName")] [ValidateNotNullOrEmpty()] [string] $Name, [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = "ByPattern")] [ValidateNotNullOrEmpty()] [string] $NamePattern, [Parameter(Mandatory, ParameterSetName = "ById")] [ValidateNotNullOrEmpty()] [string] $Id ) process { try { if ((GetDatabasePath)) { $table = 'usersandgroups' [UserMembership[]] $results = @() if ($PSCmdlet.ParameterSetName -eq 'ByName') { $user = Find-User -Name $Name } if ($PSCmdlet.ParameterSetName -eq 'ByPattern') { $user = Find-User -NamePattern $NamePattern } if ($PSCmdlet.ParameterSetName -eq 'ById') { $user = Find-User -Id $Id } if ($user) { $user | ForEach-Object { $result = Query -TableName $table -Condition "WHERE userid = '$($_.Id)'" $obj = [PSCustomObject]@{ UserName = $_.DisplayName UserId = $_.Id } $groups = [PSCustomObject]@{ GroupName = $result.DisplayName GroupId = $result.GroupId } Add-Member -InputObject $obj -MemberType NoteProperty -Name "Membership" -Value $groups -TypeName PSCustomObject $results += $obj } return $results } } else { throw "Couldn't find the database in provided path. Please run 'Set-BackupPath' cmdlet to set the database path." } } catch { Write-Error "An error occurred: $($_.Exception.Message)." } } } function Restore-AzADSecurityGroup { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseUsingScopeModifierInNewRunspaces', '', Justification = 'Using ArgumentList')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'All the paramerers are declared and used within scriptblock')] [CmdletBinding(DefaultParameterSetName = 'ByName', HelpUri = "https://github.com/hkarthik7/azure-ad-recovery-manager/blob/main/src/docs/Restore-AzADSecurityGroup.md")] param ( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'ByName')] [ValidateNotNullOrEmpty()] [string] $GroupDisplayName, [Parameter(Mandatory, ParameterSetName = 'All')] [switch] $RestoreAll ) process { ValidateLogin try { if ((GetDatabasePath)) { if ($PSCmdlet.ParameterSetName -eq 'ByName') { $groupsToRestore = (Find-Group -Name $GroupDisplayName).Id } if (($PSCmdlet.ParameterSetName -eq 'All') -or ($RestoreAll.IsPresent)) { $groups = Get-AzADGroup $groups = $groups | Where-Object { !$_.MailEnabled } $backupGroups = Find-Group -All | Where-Object { !$_.MailEnabled } $groupsToRestore = Compare-Object -ReferenceObject $groups.Id -DifferenceObject $backupGroups.Id | Select-Object -ExpandProperty InputObject } if ($groupsToRestore) { [Group[]] $allGroups = @() foreach ($groupId in $groupsToRestore) { if (!(IsGroupExists -GroupId $groupId)) { $members = Find-GroupMemberShip -Id $groupId $roleAssignment = Find-RoleAssignment -Id $groupId $group = Find-Group -Id $groupId Write-Verbose "Restoring group [$($group.DisplayName)]" $newGrp = New-AzADGroup ` -DisplayName $group.DisplayName ` -Description $group.Description ` -MailNickname $group.MailNickname $newGroup = Get-AzADGroup -ObjectId $newGrp.Id if ($roleAssignment) { $job = Start-Job -Name 'role-assignment' -ScriptBlock { param([object[]] $RoleAssignment, [string] $GroupId) Start-Sleep -Seconds 60 # Wait for group to get reflected $RoleAssignment | ForEach-Object { New-AzRoleAssignment -ObjectId $GroupId -RoleDefinitionId $_.RoleDefinitionId -Scope $_.Scope } } -ArgumentList ($roleAssignment, $newGroup.Id) } $allGroups += [Group]@{ Id = $newGroup.Id DisplayName = $newGroup.DisplayName MailNickname = $newGroup.MailNickname Description = $newGroup.Description CreatedDateTime = $newGroup.CreatedDateTime IsAssignableToRole = $newGroup.IsAssignableToRole Owner = $newGroup.Owner RenewedDateTime = $newGroup.RenewedDateTime SecurityEnabled = $newGroup.SecurityEnabled SecurityIdentifier = $newGroup.SecurityIdentifier } Invoke-SqliteQuery -DataSource (GetDatabasePath) -Query "DELETE FROM groups WHERE id = '$groupId'" Invoke-SqliteBulkCopy -DataTable ($allGroups | Out-DataTable) -DataSource (GetDatabasePath) -Table 'groups' -ConflictClause Ignore -Force if ($members) { Add-AzADGroupMember -TargetGroupObjectId $newGroup.Id -MemberObjectId $members.Members.UserId -WarningAction SilentlyContinue foreach ($userId in $members.Members.UserId) { Invoke-SqliteQuery -DataSource (GetDatabasePath) -Query "INSERT INTO usersandgroups (userid, groupid, odatatype, displayname) VALUES ( (SELECT id FROM users WHERE id = '$userId'), '$($newGroup.Id)', '$($newGroup.odatatype)', '$($newGroup.DisplayName)' )" } } } else { Write-Warning "Group $((Find-Group -Id $groupId).DisplayName) already exists." } } $report = [RestoreReport]@{ RestoredDateTime = Get-Date GroupsRestored = $allGroups.DisplayName -join "`n" } if ($roleAssignment -and $job) { $roleAssignmentResults = $job | Wait-Job | Receive-Job if ($roleAssignmentResults) { Write-Verbose "Successfully completed restoring the role assignment for group(s) [$($roleAssignmentResults.DisplayName -join ", ")]." Add-Member -InputObject $report -MemberType NoteProperty -Name 'RoleAssignmentName' -Value ($roleAssignmentResults.RoleAssignmentName -join "`n") -TypeName RestoreReport -Force Add-Member -InputObject $report -MemberType NoteProperty -Name 'RoleAssignmentId' -Value ($roleAssignmentResults.RoleAssignmentId -join "`n") -TypeName RestoreReport -Force Add-Member -InputObject $report -MemberType NoteProperty -Name 'Scope' -Value ($roleAssignmentResults.Scope -join "`n") -TypeName RestoreReport -Force Add-Member -InputObject $report -MemberType NoteProperty -Name 'DisplayName' -Value ($roleAssignmentResults.DisplayName -join "`n") -TypeName RestoreReport -Force Add-Member -InputObject $report -MemberType NoteProperty -Name 'SignInName' -Value ($roleAssignmentResults.SignInName -join "`n") -TypeName RestoreReport -Force Add-Member -InputObject $report -MemberType NoteProperty -Name 'RoleDefinitionName' -Value ($roleAssignmentResults.RoleDefinitionName -join "`n") -TypeName RestoreReport -Force Add-Member -InputObject $report -MemberType NoteProperty -Name 'RoleDefinitionId' -Value ($roleAssignmentResults.RoleDefinitionId -join "`n") -TypeName RestoreReport -Force Add-Member -InputObject $report -MemberType NoteProperty -Name 'ObjectId' -Value ($roleAssignmentResults.ObjectId -join "`n") -TypeName RestoreReport -Force } } $report | Export-Csv -Path "$(Split-Path -Path (GetDatabasePath) -Parent)\Azure-AD-Restore-Report.csv" -Encoding utf8 -Force -NoTypeInformation -Append return $allGroups } else { Write-Warning "No groups found to restore." } } else { throw "Couldn't find the database in provided path. Please run 'Set-BackupPath' cmdlet to set the database path." } } catch { Write-Error "An Error Occurred at line $($_.InvocationInfo.ScriptLineNumber). Message: $($_.Exception.Message)." } } } function Set-BackupPath { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'No state changing functions')] [CmdletBinding(HelpUri = "https://github.com/hkarthik7/azure-ad-recovery-manager/blob/main/src/docs/Set-BackupPath.md")] param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline, Position = 0)] [ValidateNotNullOrEmpty()] [ValidateScript({ Test-Path $_ })] [string] $FilePath ) process { try { $path = Resolve-Path -Path $FilePath.TrimEnd("\") [System.Environment]::SetEnvironmentVariable('AZURE_AD_BACKUP_DATABASE', "$path\Azure-AD-Backup.db", [System.EnvironmentVariableTarget]::Process) } catch { Write-Error "An Error Occurred at line $($_.InvocationInfo.ScriptLineNumber). Message: $($_.Exception.Message)." } } } |