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)."
        }
    }
}