DFIR-O365RC.psm1

function Write-Log {
    param
    (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [ValidateNotNullOrEmpty()]
        [String]$message,
        [Parameter(Mandatory=$true)]
        [Alias("LogPath")]
        [String]$path,
        [Parameter(Mandatory=$false)]
        [ValidateSet("Error","Warning","Info")]
        [Alias("LogLevel")]
        [String]$level="Info"
    )

    $logTime = "{0:yyyy-MM-dd} {0:HH:mm:ss}" -f (Get-Date) + ","

    switch ($level){
        "Error" {
            $levelText = "ERROR,"
        }
        "Warning" {
            $levelText = "WARNING,"
        }
        "Info" {
            $levelText = "INFO,"
        }
    }

    "$logTime $levelText $message" | Out-File -FilePath $path -Append -Encoding UTF8
}

function Import-Certificate {
    param (
        [Parameter(Mandatory = $true)]
        [String]$logFile,
        [Parameter(Mandatory = $true)]
        [String]$certificatePath
    )

    if (-not (Test-Path -Path $certificatePath)){
        Write-Error "The provided path for certificate: $certificatePath does not exist. Exiting"
        "The provided path for certificate: $certificatePath does not exist. Exiting" | Write-Log -LogPath $logFile -LogLevel "ERROR"
        exit
    }

    "Loading certificate $certificatePath" | Write-Log -LogPath $logFile
    try {
        $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($certificatePath)
        Write-Host "Loaded certificate $certificatePath with no password"
        "Loaded certificate $certificatePath with no password" | Write-Log -LogPath $logFile
        $emptySecurePassword = New-Object System.Security.SecureString
        return $cert, $false, $emptySecurePassword
    }
    catch {
        $errorMessage = $_.Exception.ToString()
        if ($errorMessage.Contains("password")){
            try {
                $certificatePassword = Read-Host -MaskInput "Please enter the password for the certificate $certificatePath"
                $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($certificatePath,$certificatePassword)
                $certificateSecurePassword = ConvertTo-SecureString -String $certificatePassword -AsPlainText -Force
                Write-Host "Loaded certificate $certificatePath with a password"
                "Loaded certificate $certificatePath with a password" | Write-Log -LogPath $logFile
                return $cert, $true, $certificateSecurePassword
            }
            catch {
                $errorMessage = $_.Exception.ToString()
                if ($errorMessage.Contains("password")){
                    Write-Error "Wrong password was provided for certificate $certificatePath. Exiting"
                    "Wrong password was provided for certificate $certificatePath. Exiting" | Write-Log -LogPath $logFile -LogLevel "ERROR"
                    exit
                }
                else {
                    Write-Error "Error while loading the certificate: $errorMessage. Exiting"
                    "Error while loading the certificate: $errorMessage. Exiting" | Write-Log -LogPath $logFile -LogLevel "ERROR"
                    exit
                }
            }
        }
        else {
            Write-Error "Error while loading the certificate: $errorMessage. Exiting"
            "Error while loading the certificate: $errorMessage. Exiting" | Write-Log -LogPath $logFile -LogLevel "ERROR"
            exit
        }
    }
}

function Connect-AzUser {
    param (
        [Parameter(Mandatory = $true)]
        [String]$logFile
    )

    $stopLoop = $false
    [Int]$retryCount = "0"

    try {
        $null = Disconnect-AzAccount -ErrorAction Stop
    }
    catch {
        
    }
    do {
        try {
            "Connecting to Azure" | Write-Log -LogPath $logFile
            Write-Warning "Please log in to Azure using a privileged account"
            $null = Connect-AzAccount -DeviceAuth -Confirm:$false -ErrorAction Stop
            "Successfully logged in to Azure" | Write-Log -LogPath $logFile
            $stopLoop = $true
        }
        catch {
            if ($retryCount -ge 3){
                Write-Error "Failed to log in to Azure $($retryCount + 1) times - aborting"
                "Failed to log in to Azure $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error"
                $stopLoop = $true
            }
            else {
                $errorMessage = $_.Exception.Message
                Write-Warning "Failed to log in to Azure $($retryCount + 1) times - sleeping and retrying - $errorMessage"
                "Failed to log in to Azure $($retryCount + 1) times - sleeping and retrying - $errorMessage" | Write-Log -LogPath $logFile -LogLevel "Warning"
                Start-Sleep -Seconds (60 * ($retryCount + 1))
                $retryCount = $retryCount + 1
            }
        }
    } while ($stopLoop -eq $false)
}

function Connect-AzApplication {
    param (
        [Parameter(Mandatory = $true)]
        [String]$logFile,
        [Parameter(Mandatory = $true)]
        [String]$certificatePath,
        [Parameter(Mandatory = $true)]
        [SecureString]$certificateSecurePassword,
        [Parameter(Mandatory = $true)]
        [Bool]$needPassword,
        [Parameter(Mandatory = $true)]
        [String]$tenant,
        [Parameter(Mandatory = $true)]
        [String]$appId
    )

    $stopLoop = $false
    [Int]$retryCount = "0"
    do {
        try {
            try {
                $null = Disconnect-AzAccount -ErrorAction Stop
            }
            catch {
                
            }
            if ($needPassword){
                $null = Connect-AzAccount -CertificatePath $certificatePath -CertificatePassword $certificateSecurePassword -ServicePrincipal -Tenant $tenant -ApplicationId $appId -ErrorAction Stop
            }
            else {
                $null = Connect-AzAccount -CertificatePath $certificatePath -ServicePrincipal -Tenant $tenant -ApplicationId $appId -ErrorAction Stop
            }
            "Successfully logged in to Az using application $appId" | Write-Log -LogPath $logFile
            $stopLoop = $true
        }
        catch {
            if ($retryCount -ge 3){
                "Failed to log in to Az using application $appId $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error"
                $stopLoop = $true
            }
            else {
                $errorMessage = $_.Exception.Message
                "Failed to log in to Az using application $appId $($retryCount + 1) times - sleeping and retrying - $($errorMessage)" | Write-Log -LogPath $logFile -LogLevel "Warning"
                Start-Sleep -Seconds (60 * ($retryCount + 1))
                $retryCount = $retryCount + 1
            }
        }
    } while ($stopLoop -eq $false)
}

function Connect-ExchangeOnlineUser {
    param (
        [Parameter(Mandatory = $true)]
        [String]$logFile
    )

    $stopLoop = $false
    [Int]$retryCount = "0"

    try {
        $null = Disconnect-ExchangeOnline -Confirm:$false -ErrorAction Stop
    }
    catch {
        
    }
    do {
        try {
            "Connecting to Exchange Online" | Write-Log -LogPath $logFile
            Write-Warning "Please log in to Exchange Online using a privileged account"
            Connect-ExchangeOnline -ErrorAction Stop -Device -ShowBanner:$false
            "Successfully logged in to Exchange Online" | Write-Log -LogPath $logFile
            $stopLoop = $true
        }
        catch {
            if ($retryCount -ge 3){
                Write-Error "Failed to log in to Exchange Online $($retryCount + 1) times - aborting"
                "Failed to log in to Exchange Online $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error"
                $stopLoop = $true
            }
            else {
                $errorMessage = $_.Exception.Message
                Write-Warning "Failed to log in to Exchange Online $($retryCount + 1) times - sleeping and retrying - $errorMessage"
                "Failed to log in to Exchange Online $($retryCount + 1) times - sleeping and retrying - $errorMessage" | Write-Log -LogPath $logFile -LogLevel "Warning"
                Start-Sleep -Seconds (60 * ($retryCount + 1))
                $retryCount = $retryCount + 1
            }
        }
    } while ($stopLoop -eq $false)
}

function Connect-ExchangeOnlineApplication {

    param (
        [Parameter(Mandatory = $true)]
        [String]$logFile,
        [Parameter(Mandatory = $true)]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate,
        [Parameter(Mandatory = $true)]
        [String]$appId,
        [Parameter(Mandatory = $true)]
        [String]$organization,
        [Parameter(Mandatory = $false)]
        [Array]$commandNames = @("Search-UnifiedAuditLog","Search-MailboxAuditLog")
    )

    $stopLoop = $false
    [Int]$retryCount = "0"
    do {
        try {
            try {
                Disconnect-ExchangeOnline -Confirm:$false -ErrorAction Stop
            }
            catch {
                
            }
            Connect-ExchangeOnline -Certificate $certificate -AppID $appId -Organization $organization -CommandName $commandNames -ErrorAction Stop -ShowBanner:$false
            "Successfully logged in to Exchange Online using application $appId" | Write-Log -LogPath $logFile
            $stopLoop = $true
        }
        catch {
            if ($retryCount -ge 3){
                "Failed to log in to Exchange Online using application $appId $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error"
                $stopLoop = $true
            }
            else {
                $errorMessage = $_.Exception.Message
                "Failed to log in to Exchange Online using application $appId $($retryCount + 1) times - sleeping and retrying - $($errorMessage)" | Write-Log -LogPath $logFile -LogLevel "Warning"
                if ($errorMessage -like "*No cmdlet assigned to the user have this feature enabled.*" -or $errorMessage -eq "UnAuthorized"){
                    $retryCount = 3
                }
                else {
                    Start-Sleep -Seconds (60 * ($retryCount + 1))
                }
                $retryCount = $retryCount + 1
            }
        }
    } while ($stopLoop -eq $false)
}

function Connect-MicrosoftGraphUser {
    param (
        [Parameter(Mandatory = $true)]
        [String]$logFile
    )

    $stopLoop = $false
    [Int]$retryCount = "0"
    try {
        $null = Disconnect-MgGraph -ErrorAction Stop
    }
    catch {
        
    }
    do {
        try {
            "Connecting to Entra ID" | Write-Log -LogPath $logFile
            Write-Warning "Please log in to Entra ID using a privileged account"
            Connect-MgGraph -NoWelcome -Scopes "Application.ReadWrite.All, Directory.Read.All, GroupMember.ReadWrite.All, RoleManagement.ReadWrite.Directory" -UseDeviceCode -ErrorAction Stop
            "Successfully logged in to Entra ID" | Write-Log -LogPath $logFile
            $stopLoop = $true
        }
        catch {
            if ($retryCount -ge 3){
                Write-Error "Failed to log in to Entra ID $($retryCount + 1) times - aborting"
                "Failed to log in to Entra ID $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error"
                $stopLoop = $true
            }
            else {
                $errorMessage = $_.Exception.Message
                Write-Warning "Failed to log in to Entra ID $($retryCount + 1) times - sleeping and retrying - $errorMessage"
                "Failed to log in to Entra ID $($retryCount + 1) times - sleeping and retrying - $errorMessage" | Write-Log -LogPath $logFile -LogLevel "Warning"
                Start-Sleep -Seconds (60 * ($retryCount + 1))
                $retryCount = $retryCount + 1
            }
        }
    } while ($stopLoop -eq $false)
}

function Connect-MicrosoftGraphApplication {

    param (
        [Parameter(Mandatory = $true)]
        [String]$logFile,
        [Parameter(Mandatory = $true)]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate,
        [Parameter(Mandatory = $true)]
        [String]$appId,
        [Parameter(Mandatory = $true)]
        [String]$tenant
    )

    $stopLoop = $false
    [Int]$retryCount = "0"
    do {
        try {
            try {
                $null = Disconnect-MgGraph -ErrorAction Stop
            }
            catch {

            }
            Connect-MgGraph -Certificate $certificate -ClientId $appId -TenantId $tenant -NoWelcome -ErrorAction Stop
            "Successfully logged in to Microsoft Graph using application $appId" | Write-Log -LogPath $logFile
            $stopLoop = $true
        }
        catch {
            if ($retryCount -ge 3){
                "Failed to log in to Microsoft Graph using application $appId $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error"
                $stopLoop = $true
            }
            else {
                $errorMessage = $_.Exception.Message
                "Failed to log in to Microsoft Graph using application $appId $($retryCount + 1) times - sleeping and retrying - $errorMessage" | Write-Log -LogPath $logFile -LogLevel "Warning"
                Start-Sleep -Seconds (60 * ($retryCount + 1))
                $retryCount = $retryCount + 1
            }
        }
    } while ($stopLoop -eq $false)
}

function Get-AzDevOpsRestAPIResponseUser {
    param
    (
        [Parameter(Mandatory = $true)]
        [String]$uri,
        [Parameter(Mandatory = $true)]
        [String]$logFile
    )
    try {
        $token = Get-AzAccessToken -ResourceUrl "499b84ac-1321-427f-aa17-267ca6975798" -AsSecureString:$false -ErrorAction Stop
    }
    catch {
        Connect-AzUser -logFile $logFile
        $token = Get-AzAccessToken -ResourceUrl "499b84ac-1321-427f-aa17-267ca6975798" -AsSecureString:$false -ErrorAction Stop
    }

    $APIresults = @()

    $stopLoop = $false
    [Int]$retryCount = "0"
    while ($stopLoop -eq $false){
        try {
            $data = Invoke-RestMethod -Headers @{Authorization = "Bearer $($token.Token)"} -Uri $($uri) -Method GET -ContentType "application/json" -ResponseHeadersVariable responseHeaders -ErrorAction Stop
            $stopLoop = $true
        }
        catch {
            if ($retryCount -ge 10){
                Write-Error "Failed to dump events from Azure DevOps URI $($uri) $($retryCount + 1) times - aborting"
                "Failed to dump events from Azure DevOps URI $($uri) $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error"
                $data = @()
                $stopLoop = $true
            }
            else {
                $errorCode = $_.Exception.Response.StatusCode.value__
                $errorMessage = $_.ErrorDetails.Message
                Write-Error "Failed to dump events from Azure DevOps URI $($uri) $($retryCount + 1) times - sleeping and retrying - ${errorCode}: ${errorMessage}"
                "Failed to dump events from Azure DevOps URI $($uri) $($retryCount + 1) times - sleeping and retrying - ${errorCode}: ${errorMessage}" | Write-Log -LogPath $logFile -LogLevel "Warning"
                if ($errorCode -eq "429"){
                    Start-Sleep -Seconds (15 * ($retryCount + 1))
                }
                elseif ($errorCode -eq "401" -or $errorCode -eq "403"){
                    $retryCount = 10
                }
                else {
                    Start-Sleep -Seconds 1
                }
                $retryCount = $retryCount + 1
            }
        }
    }
    if ($data){
        $APIresults += $data.value

        if ($null -ne $($responseHeaders."X-MS-ContinuationToken")){
            $stopLoop = $false
            [Int]$retryCount = "0"
            while ($stopLoop -eq $false -and $null -ne $($responseHeaders."X-MS-ContinuationToken")){
                try {
                    $continuationToken = $responseHeaders."X-MS-ContinuationToken"
                    if ($uri.contains("continuationToken=")){
                        $uri = ($uri -Split "continuationToken=")[0]
                        $uri = $uri.Substring(0, $uri.Length - 1)
                    }
                    if ($uri.contains("?")){
                        $uri += "&continuationToken=$continuationToken"
                    }
                    else {
                        $uri += "?continuationToken=$continuationToken"
                    }
                    $data = Invoke-RestMethod -Uri $uri -Headers @{Authorization = "Bearer $($token.Token)"} -Method GET -ContentType "application/json" -ResponseHeadersVariable responseHeaders -ErrorAction Stop
                    $APIresults += $data.value
                }
                catch {
                    if ($retryCount -ge 10){
                        Write-Error "Failed to dump events from Azure DevOps URI $($uri) $($retryCount + 1) times - aborting"
                        "Failed to dump events from Azure DevOps URI $($uri) $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error"
                            $data = @()
                            $stopLoop = $true
                    }
                    else {
                        $errorCode = $_.Exception.Response.StatusCode.value__
                        $errorMessage = $_.ErrorDetails.Message
                        Write-Warning "Failed to dump events from Azure DevOps URI $($uri) $($retryCount + 1) times - sleeping and retrying - ${errorCode}: ${errorMessage}"
                        "Failed to dump events from Azure DevOps URI $($uri) $($retryCount + 1) times - sleeping and retrying - ${errorCode}: ${errorMessage}" | Write-Log -LogPath $logFile -LogLevel "Warning"
                        if ($token.ExpiresOn -le (Get-Date)){
                            Write-Warning "Token has expired, renewing"
                            "Token has expired, renewing" | Write-Log -LogPath $logFile -LogLevel "Warning"
                            try {
                                $token = Get-AzAccessToken -ResourceUrl "499b84ac-1321-427f-aa17-267ca6975798" -AsSecureString:$false -ErrorAction Stop
                            }
                            catch {
                                Connect-AzUser -logFile $logFile
                                $token = Get-AzAccessToken -ResourceUrl "499b84ac-1321-427f-aa17-267ca6975798" -AsSecureString:$false -ErrorAction Stop
                            }
                        }
                        else {
                            Start-Sleep -Seconds (5 * ($retryCount + 1))
                        }
                        $retryCount = $retryCount + 1
                    }
                }
            }
        }
    }
    else {
        Write-Host "No events to dump from Azure DevOps URI $($uri)"
        "No events to dump from Azure DevOps URI $($uri)" | Write-Log -LogPath $logFile
    }
    return $APIresults
}

function Get-AzDevOpsAuditLogs {
    param
    (
        [Parameter(Mandatory = $true)]
        [String]$certificatePath,
        [Parameter(Mandatory = $true)]
        [SecureString]$certificateSecurePassword,
        [Parameter(Mandatory = $true)]
        [Bool]$needPassword,
        [Parameter(Mandatory = $true)]
        [String]$tenant,
        [Parameter(Mandatory = $true)]
        [String]$appId,
        [Parameter(Mandatory = $true)]
        [String]$uri,
        [Parameter(Mandatory = $true)]
        [String]$logFile
    )
    try {
        $token = Get-AzAccessToken -ResourceUrl "499b84ac-1321-427f-aa17-267ca6975798" -AsSecureString:$false -ErrorAction Stop
    }
    catch {
        Connect-AzApplication -logFile $logFile -certificatePath $certificatePath -certificateSecurePassword $certificateSecurePassword -needPassword $needPassword -tenant $tenant -appId $appId
        $token = Get-AzAccessToken -ResourceUrl "499b84ac-1321-427f-aa17-267ca6975798" -AsSecureString:$false -ErrorAction Stop
    }

    $APIresults = @()

    $stopLoop = $false
    [Int]$retryCount = "0"
    while ($stopLoop -eq $false){
        try {
            $data = Invoke-RestMethod -Headers @{Authorization = "Bearer $($token.Token)"} -Uri $($uri + "&batchSize=1") -Method GET -ContentType "application/json" -ErrorAction Stop
            $stopLoop = $true
        }
        catch {
            if ($retryCount -ge 10){
                "Failed to dump events from Azure DevOps URI $($uri) $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error"
                $data = @()
                $stopLoop = $true
            }
            else {
                $errorCode = $_.Exception.Response.StatusCode.value__
                $errorMessage = $_.ErrorDetails.Message
                "Failed to dump events from Azure DevOps URI $($uri) $($retryCount + 1) times - sleeping and retrying - ${errorCode}: ${errorMessage}" | Write-Log -LogPath $logFile -LogLevel "Warning"
                if ($errorCode -eq "429"){
                    Start-Sleep -Seconds (15 * ($retryCount + 1))
                }
                elseif ($errorCode -eq "401" -or $errorCode -eq "403"){
                    $retryCount = 10
                }
                else {
                    Start-Sleep -Seconds 1
                }
                $retryCount = $retryCount + 1
            }
        }
    }
    if ($data){
        $APIresults += $data.decoratedAuditLogEntries
        
        if ($data.hasMore -eq $true){
            $stopLoop = $false
            [Int]$retryCount = "0"
            while ($stopLoop -eq $false -and $data.hasMore -eq $true){
                try {
                    $continuationToken = $data.continuationToken
                    if ($uri.contains("&continuationToken=")){
                        $uri = ($uri -Split "&continuationToken=")[0]
                    }
                    $uri += "&continuationToken=$continuationToken"
                    $data = Invoke-RestMethod -Uri $uri -Headers @{Authorization = "Bearer $($token.Token)"} -Method GET -ContentType "application/json" -ErrorAction Stop
                    $APIresults += $data.decoratedAuditLogEntries
                }
                catch {
                    if ($retryCount -ge 10){
                        "Failed to dump events from Azure DevOps URI $($uri) $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error"
                        $data = @()
                        $stopLoop = $true
                    }
                    else {
                        $errorCode = $_.Exception.Response.StatusCode.value__
                        $errorMessage = $_.ErrorDetails.Message
                        "Failed to dump events from Azure DevOps URI $($uri) $($retryCount + 1) times - sleeping and retrying - ${errorCode}: ${errorMessage}" | Write-Log -LogPath $logFile -LogLevel "Warning"
                        if ($token.ExpiresOn -le (Get-Date)){
                            "Token has expired, renewing" | Write-Log -LogPath $logFile -LogLevel "Warning"
                            try {
                                $token = Get-AzAccessToken -ResourceUrl "499b84ac-1321-427f-aa17-267ca6975798" -AsSecureString:$false -ErrorAction Stop
                            }
                            catch {
                                Connect-AzApplication -logFile $logFile -certificatePath $certificatePath -certificateSecurePassword $certificateSecurePassword -needPassword $needPassword -tenant $tenant -appId $appId
                                $token = Get-AzAccessToken -ResourceUrl "499b84ac-1321-427f-aa17-267ca6975798" -AsSecureString:$false -ErrorAction Stop
                            }
                        }
                        else {
                            Start-Sleep -Seconds (5 * ($retryCount + 1))
                        }
                        $retryCount = $retryCount + 1
                    }
                }
            }
        }
    }
    else {
        "No events to dump from Azure DevOps URI $($uri)" | Write-Log -LogPath $logFile
    }
    return $APIresults
}

function Get-LargeUnifiedAuditLog {
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateSet("Unfiltered","Operations","RecordTypes","FreeText","IPAddresses","UserIds")]
        [String]$requestType,
        [Parameter(Mandatory = $false)]
        [String]$freeText,
        [Parameter(Mandatory = $false)]
        [string[]]$IPAddresses,
        [Parameter(Mandatory = $false)]
        [string[]]$userIds,
        [Parameter(Mandatory = $false)]
        [string[]]$operations,
        [Parameter(Mandatory = $false)]
        [String]$recordTypes,
        [Parameter(Mandatory = $true)]
        [String]$sessionName,
        [Parameter(Mandatory = $true)]
        [DateTime]$startDate,
        [Parameter(Mandatory = $true)]
        [DateTime]$endDate,
        [Parameter(Mandatory = $true)]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate,
        [Parameter(Mandatory = $true)]
        [String]$appId,
        [Parameter(Mandatory = $true)]
        [String]$tenant,
        [Parameter(Mandatory = $true)]
        [String]$logFile,
        [Parameter(Mandatory = $true)]
        [String]$outputFile
    )

    "Collecting $requestType events for $startDate - $endDate" | Write-Log -LogPath $logFile -LogLevel "Info"
    [Int]$lastUnifiedAuditLogEntriesResultIndex = "0"
    do {
        $stopLoop = $false
        [Int]$retryCount = "0"
        do {
            try {
                # Using ReturnLargeSet to get back up to 50k events, 5k at a time
                if ($requestType -eq "Unfiltered"){
                    $unifiedAuditLogEntries = Search-UnifiedAuditLog -StartDate $startDate -EndDate $endDate -SessionId $sessionName -SessionCommand ReturnLargeSet -ResultSize 5000 -ErrorAction Stop                  
                }
                if ($requestType -eq "RecordTypes"){
                    $unifiedAuditLogEntries = Search-UnifiedAuditLog -StartDate $startDate -EndDate $endDate -RecordType $recordTypes -SessionId $sessionName -SessionCommand ReturnLargeSet -ResultSize 5000 -ErrorAction Stop
                }
                elseif ($requestType -eq "Operations"){
                    $unifiedAuditLogEntries = Search-UnifiedAuditLog -StartDate $startDate -EndDate $endDate -Operations $operations -SessionId $sessionName -SessionCommand ReturnLargeSet -ResultSize 5000 -ErrorAction Stop
                }
                elseif ($requestType -eq "FreeText"){
                    $unifiedAuditLogEntries = Search-UnifiedAuditLog -StartDate $startDate -EndDate $endDate -FreeText $freeText -SessionId $sessionName -SessionCommand ReturnLargeSet -ResultSize 5000 -ErrorAction Stop
                }
                elseif ($requestType -eq "IPAddresses"){
                    $unifiedAuditLogEntries = Search-UnifiedAuditLog -StartDate $startDate -EndDate $endDate -IPAddresses $IPAddresses -SessionId $sessionName -SessionCommand ReturnLargeSet -ResultSize 5000 -ErrorAction Stop
                }
                elseif ($requestType -eq "UserIds"){
                    $unifiedAuditLogEntries = Search-UnifiedAuditLog -StartDate $startDate -EndDate $endDate -UserIds $userIds -SessionId $sessionName -SessionCommand ReturnLargeSet -ResultSize 5000 -ErrorAction Stop
                }

                if ($null -eq $unifiedAuditLogEntries){
                    if ($lastUnifiedAuditLogEntriesResultIndex -ne 0){
                        throw "We were supposed to have some events, but we got an empty result instead. This might be because of a server timeout"
                    }
                    else {
                        "0 $($requestType) events between {0:yyyy-MM-dd} {0:HH:mm:ss} and {1:yyyy-MM-dd} {1:HH:mm:ss} were found" -f ($startDate, $endDate) | Write-Log -LogPath $logFile -LogLevel "Warning"
                        $countUnifiedAuditLogEntries = 0
                        break
                    }
                }

                $countUnifiedAuditLogEntries = ($unifiedAuditLogEntries | Measure-Object).Count
                $unifiedAuditLogEntriesResultCount = ($unifiedAuditLogEntries | Select-Object -Property ResultCount -Unique).ResultCount
                $unifiedAuditLogEntriesResultIndex = $unifiedAuditLogEntries[-1].ResultIndex

                if (($unifiedAuditLogEntriesResultCount -eq 0) -or ($unifiedAuditLogEntriesResultIndex -eq -1)){
                    throw "We were supposed to have some events, but we got a boggus result instead (ResultCount = 0 or ResultIndex = -1). This might be because of a server timeout"
                }
                elseif (($lastUnifiedAuditLogEntriesResultIndex + $countUnifiedAuditLogEntries) -ne $unifiedAuditLogEntriesResultIndex){
                    throw "We did not get the expected record index (lastIndex + actualCount != actualIndex). This might be because of a server timeout"
                }

                if ($lastUnifiedAuditLogEntriesResultIndex -eq 0 -and $unifiedAuditLogEntriesResultCount -gt 50000){
                    "More than 50000 $($requestType) events between {0:yyyy-MM-dd} {0:HH:mm:ss} and {1:yyyy-MM-dd} {1:HH:mm:ss} - some events will be missing" -f ($startDate, $endDate) | Write-Log -LogPath $logFile -LogLevel "Warning"
                }

                "Collected $($unifiedAuditLogEntriesResultIndex) events out of $($unifiedAuditLogEntriesResultCount) (+$($countUnifiedAuditLogEntries))" | Write-Log -LogPath $logFile -LogLevel "Info"
                $stopLoop = $true
            }
            catch {
                $lastUnifiedAuditLogEntriesResultIndex = 0
                $countUnifiedAuditLogEntries = 0
                $unifiedAuditLogEntries = @()
                if ($retryCount -ge 10){
                    "Failed to dump $($requestType) events $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error"
                    $stopLoop = $true
                }
                else {
                    $errorMessage = $_.Exception.Message
                    "Failed to dump $($requestType) events $($retryCount + 1) times - deleting, reconnecting, sleeping and retrying for the time period $startDate - $endDate - $($errorMessage)" | Write-Log -LogPath $logFile -LogLevel "Warning"
                    $sessionName = $(New-Guid).Guid
                    if ((Test-Path $outputFile) -eq $true){
                        $null = Remove-Item $outputFile -Force -Confirm:$false
                    }
                    Start-Sleep -Seconds (60 * ($retryCount + 1))
                    Connect-ExchangeOnlineApplication -logFile $logFile -certificate $cert -appId $appId -organization $tenant
                    $retryCount = $retryCount + 1
                }
            }
        } while ($stopLoop -eq $false)

         # If count is 0, no events to process
        if ($countUnifiedAuditLogEntries -gt 0){
             # Dump data to json file
            $unifiedAuditLogEntries | Select-Object -ExpandProperty AuditData | Out-File $outputFile -Encoding UTF8 -Append
            $lastUnifiedAuditLogEntriesResultIndex += $countUnifiedAuditLogEntries
            if ($unifiedAuditLogEntriesResultIndex -ne 0 -and (($unifiedAuditLogEntriesResultIndex -eq $unifiedAuditLogEntriesResultCount) -or ($unifiedAuditLogEntriesResultIndex -eq 50000))){
                "Done collecting events for ${startDate} - ${endDate}: ${unifiedAuditLogEntriesResultIndex} events were collected out of $($unifiedAuditLogEntriesResultCount)" | Write-Log -LogPath $logFile -LogLevel "Info"
                $countUnifiedAuditLogEntries = 0
            }
        }
    } until ($countUnifiedAuditLogEntries -eq 0)
}

function Get-MicrosoftGraphLogs {
    param
    (
        [Parameter(Mandatory = $true)]
        [String]$type,
        [Parameter(Mandatory = $false)]
        [String]$tenantSize,
        [Parameter(Mandatory = $true)]
        [String]$dateStart,
        [Parameter(Mandatory = $true)]
        [String]$dateEnd,
        [Parameter(Mandatory = $true)]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate,
        [Parameter(Mandatory = $true)]
        [String]$appId,
        [Parameter(Mandatory = $true)]
        [String]$tenant,
        [Parameter(Mandatory = $true)]
        [String]$logFile
    )
    $stopLoop = $false
    [Int]$retryCount = "0"
    do {
        try {
            if ($type -eq "SignIns"){
                if ($tenantSize -eq "normal"){
                    $AzureADEvents = Get-MgBetaAuditLogSignIn -All -Filter "createdDateTime ge $($dateStart) and createdDateTime lt $($dateEnd)" -ErrorAction Stop
                }
                else {
                    $AzureADEvents = Get-MgBetaAuditLogSignIn -All -Filter "createdDateTime ge $($dateStart) and createdDateTime lt $($dateEnd) and status/errorCode eq 0 and (appId eq '00000002-0000-0ff1-ce00-000000000000' or appId eq '1b730954-1685-4b74-9bfd-dac224a7b894' or appId eq 'a0c73c16-a7e3-4564-9a95-2bdf47383716' or appId eq '00000003-0000-0ff1-ce00-000000000000' or appId eq '6eb59a73-39b2-4c23-a70f-e2e3ce8965b1' or appId eq 'cb1056e2-e479-49de-ae31-7812af012ed8' or appId eq '1950a258-227b-4e31-a9cf-717495945fc2' or appId eq 'fb78d390-0c51-40cd-8e17-fdbfab77341b' or appId eq '04b07795-8ddb-461a-bbee-02f9e1bf7b46')" -ErrorAction Stop
                }
            }
            elseif ($type -eq "AuditLogs"){
                    $AzureADEvents = Get-MgBetaAuditLogDirectoryAudit -All -Filter "activityDateTime ge $($dateStart) and activityDateTime lt $($dateEnd)" -ErrorAction Stop
            }
            $stopLoop = $true
        }
        catch {
            if ($retryCount -ge 10){
                "Failed to get $($type) logs $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error"
                $AzureADEvents = $null
                $stopLoop = $true
            }
            else {
                $errorMessage = $_.Exception.Message
                if ($errorMessage -ne "Too many retries performed"){
                    Start-Sleep -Seconds (60 * ($retryCount + 1) + $(Get-Random -Minimum 1 -Maximum 60))
                }
                "Failed to get $($type) logs $($retryCount + 1) times - reconnecting and retrying - $($errorMessage)" | Write-Log -LogPath $logFile -LogLevel "Warning"
                Connect-MicrosoftGraphApplication -certificate $certificate -appId $appId -tenant $tenant -logFile $logFile
                $retryCount = $retryCount + 1
            }
        }
    } while ($stopLoop -eq $false)
    return $AzureADEvents
}

function Get-AzureRMActivityLog {
    param
    (
        [Parameter(Mandatory = $true)]
        [String]$dateStart,
        [Parameter(Mandatory = $true)]
        [String]$dateEnd,
        [Parameter(Mandatory = $true)]
        [String]$certificatePath,
        [Parameter(Mandatory = $true)]
        [SecureString]$certificateSecurePassword,
        [Parameter(Mandatory = $true)]
        [Bool]$needPassword,
        [Parameter(Mandatory = $true)]
        [String]$appId,
        [Parameter(Mandatory = $true)]
        [String]$tenant,
        [Parameter(Mandatory = $true)]
        [String]$logFile
    )
    $stopLoop = $false
    [Int]$retryCount = "0"
    do {
        try {
            $azureRMActivityEvents = Get-AzActivityLog -StartTime $dateStart -EndTime $dateEnd -DetailedOutput -ErrorAction Stop
            $stopLoop = $true
        }
        catch {
            if ($retryCount -ge 10){
                "Failed to get Azure Resource Manager activity logs $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error"
                $azureRMActivityEvents = $null
                $stopLoop = $true
            }
            else {
                $errorMessage = $_.Exception.Message
                Start-Sleep -Seconds (60 * ($retryCount + 1) + $(Get-Random -Minimum 1 -Maximum 60))
                "Failed to get Azure Resource Manager activity logs $($retryCount + 1) times - reconnecting and retrying - $($errorMessage)" | Write-Log -LogPath $logFile -LogLevel "Warning"
                Connect-AzApplication -certificatePath $certificatePath -certificateSecurePassword $certificateSecurePassword -needPassword $needPassword -tenant $tenant -appId $appId -logFile $logFile
                $retryCount = $retryCount + 1
            }
        }
    } while ($stopLoop -eq $false)
    return $azureRMActivityEvents
}

function Get-UnifiedAuditLogPurview {
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateSet("Unfiltered","Operations","RecordTypes","FreeText","IPAddresses","UserIds")]
        [String]$requestType,
        [Parameter(Mandatory = $false)]
        [String]$freeText,
        [Parameter(Mandatory = $false)]
        [string[]]$IPAddresses,
        [Parameter(Mandatory = $false)]
        [string[]]$userIds,
        [Parameter(Mandatory = $false)]
        [string[]]$operations,
        [Parameter(Mandatory = $false)]
        [string[]]$recordTypes,
        [Parameter(Mandatory = $true)]
        [String]$sessionName,
        [Parameter(Mandatory = $true)]
        [DateTime]$startDate,
        [Parameter(Mandatory = $true)]
        [DateTime]$endDate,
        [Parameter(Mandatory = $true)]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate,
        [Parameter(Mandatory = $true)]
        [String]$appId,
        [Parameter(Mandatory = $true)]
        [String]$tenant,
        [Parameter(Mandatory = $true)]
        [String]$logFile,
        [Parameter(Mandatory = $true)]
        [String]$outputFile
    )
    $stopLoop = $false
    [Int]$retryCount = "0"
    "Collecting $requestType events for $startDate - $endDate" | Write-Log -LogPath $logFile -LogLevel "Info"
    do {
        try {
            if ($requestType -eq "Unfiltered"){
                $auditLogQuery = New-MgBetaSecurityAuditLogQuery -FilterStartDateTime $startDate -FilterEndDateTime $endDate -DisplayName $sessionName -ErrorAction Stop
            }
            if ($requestType -eq "RecordTypes"){
                $auditLogQuery = New-MgBetaSecurityAuditLogQuery -FilterStartDateTime $startDate -FilterEndDateTime $endDate -DisplayName $sessionName -RecordTypeFilters $recordTypes -ErrorAction Stop
            }
            elseif ($requestType -eq "Operations"){
                $auditLogQuery = New-MgBetaSecurityAuditLogQuery -FilterStartDateTime $startDate -FilterEndDateTime $endDate -DisplayName $sessionName -OperationFilters $operations -ErrorAction Stop
            }
            elseif ($requestType -eq "FreeText"){
                $auditLogQuery = New-MgBetaSecurityAuditLogQuery -FilterStartDateTime $startDate -FilterEndDateTime $endDate -DisplayName $sessionName -KeywordFilter $freeText -ErrorAction Stop
            }
            elseif ($requestType -eq "IPAddresses"){
                $auditLogQuery = New-MgBetaSecurityAuditLogQuery -FilterStartDateTime $startDate -FilterEndDateTime $endDate -DisplayName $sessionName -IPAddressFilters $IPAddresses -ErrorAction Stop
            }
            elseif ($requestType -eq "UserIds"){
                $auditLogQuery = New-MgBetaSecurityAuditLogQuery -FilterStartDateTime $startDate -FilterEndDateTime $endDate -DisplayName $sessionName -UserPrincipalNameFilters $userIds -ErrorAction Stop
            }
            $stopLoop = $true
        }
        catch {
            if ($retryCount -ge 10){
                "Failed to create a Purview query for $($requestType) events for $startDate - $endDate $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error"
                $auditLogQuery = $null
                $stopLoop = $true
            }
            else {
                $errorMessage = $_.Exception.Message
                Start-Sleep -Seconds (60 * ($retryCount + 1) + $(Get-Random -Minimum 1 -Maximum 60))
                "Failed to create a Purview query for $($requestType) events for $startDate - $endDate $($retryCount + 1) times - reconnecting and retrying - $($errorMessage)" | Write-Log -LogPath $logFile -LogLevel "Warning"
                Connect-MicrosoftGraphApplication -certificate $certificate -appId $appId -tenant $tenant -logFile $logFile
                $retryCount = $retryCount + 1
            }
        }
    } while ($stopLoop -eq $false)

    if ($null -ne $auditLogQuery){
        $auditLogQueryId = $auditLogQuery.Id
        "Purview query for $($requestType) events for $startDate - $endDate was created with the Id $($auditLogQueryId)" | Write-Log -LogPath $logFile -LogLevel "Info"
        $stopLoop = $false
        while (-not $stopLoop){
            try {
                $status = (Get-MgBetaSecurityAuditLogQuery -AuditLogQueryId $auditLogQueryId -ErrorAction Stop).Status
            }
            catch {
                "Failed to get status for query $auditLogQueryId. Retrying" | Write-Log -LogPath $logFile -LogLevel "Warning"
                continue
            }
            "Audit Log Query $($auditLogQueryId) is in status `"$status`"" | Write-Log -LogPath $logFile
            if ($status -eq "succeeded"){
                $stopLoop = $true
            }
            if ($status -eq "failed" -or $status -eq "cancelled"){
                "Audit Log Query $auditLogQueryId has failed: `"$status`"" | Write-Log -LogPath $logFile -LogLevel "Error"
                $stopLoop = $true
            }
            Start-Sleep -Seconds 10
        }
        if ($status -eq "succeeded"){
            $stopLoop = $false
            [Int]$retryCount = "0"
            do {
                try {
                    $records = Get-MgBetaSecurityAuditLogQueryRecord -AuditLogQueryId $auditLogQueryId -All -ErrorAction Stop
                    $stopLoop = $true
                }
                catch {
                    if ($retryCount -ge 10){
                        "Failed to get events for Purview request $auditLogQueryId $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error"
                        $records = $null
                        $stopLoop = $true
                    }
                    else {
                        $errorMessage = $_.Exception.Message
                        "Failed to get events for Purview request $auditLogQueryId $($retryCount + 1) times - reconnecting and retrying - $($errorMessage)" | Write-Log -LogPath $logFile -LogLevel "Warning"
                        Connect-MicrosoftGraphApplication -certificate $certificate -appId $appId -tenant $tenant -logFile $logFile
                        $retryCount = $retryCount + 1
                    }
                }
            } while ($stopLoop -eq $false)

            if ($null -ne $records){
                $recordsCount = $records.Count
                "Got $recordsCount events for Purview request $auditLogQueryId" | Write-Log -LogPath $logFile -LogLevel "Info"
                "[" | Out-File $outputFile -Encoding UTF8 -Append
                $i = 0
                $records | ForEach-Object {
                    if ($i % 10000 -eq 0){
                        "Wrote $i events out of $recordsCount for query $auditLogQueryId"  | Write-Log -LogPath $logFile -LogLevel "Info"
                    }
                    $_ | Select-Object -ExpandProperty AuditData | Select-Object -ExpandProperty AdditionalProperties | ConvertTo-Json -Depth 99 | Out-File $outputFile -Encoding UTF8 -Append
                    if ($i -ne ($recordsCount -1)){
                        "," | Out-File $outputFile -Encoding UTF8 -Append
                    }
                    $i += 1
                }
                "]" | Out-File $outputFile -Encoding UTF8 -Append
                "Wrote $i events out of $recordsCount for query $auditLogQueryId"  | Write-Log -LogPath $logFile -LogLevel "Info"
            }
            else {
                "Got 0 events for query $auditLogQueryId" | Write-Log -LogPath $logFile -LogLevel "Warning"
            }
        }
    }
}

function Get-MailboxAuditLog {
    param
    (
        [Parameter(Mandatory = $true)]
        [String]$outputFileWithoutJson,
        [Parameter(Mandatory = $true)]
        [System.Array]$userIds,
        [Parameter(Mandatory = $true)]
        [DateTime]$startDate,
        [Parameter(Mandatory = $true)]
        [DateTime]$endDate,
        [Parameter(Mandatory = $true)]
        [String]$logFile,
        [Parameter(Mandatory = $true)]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate,
        [Parameter(Mandatory = $true)]
        [String]$appId,
        [Parameter(Mandatory = $true)]
        [String]$tenant
    )

    foreach ($userId in $userIds){
        $mailboxAuditLogEntries = @()
        $countMailboxAuditLogEntries = 0

        "Collecting MailboxAuditLog events for $startDate - $endDate for user $userId" | Write-Log -LogPath $logFile -LogLevel "Info"
        $outputFile = "$($outputFileWithoutJson)_$($userId).json"
        $stopLoop = $false
        [Int]$retryCount = "0"
        do {
            try {
                $mailboxAuditLogEntries = Search-MailboxAuditLog -StartDate $startDate -EndDate $endDate -Identity $userId -LogonTypes Admin,Delegate,Owner -IncludeInactiveMailbox -ShowDetails -ResultSize 250000 -ErrorAction Stop
                $countMailboxAuditLogEntries = ($mailboxAuditLogEntries | Measure-Object).Count
                $stopLoop = $true
            }
            catch {
                if ($_.ToString().contains("ManagementObjectNotFoundException")){
                    "$($userId) does not have a mailbox" | Write-Log -LogPath $logFile -LogLevel "Warning"
                    $countMailboxAuditLogEntries = 0
                    $stopLoop = $true
                }
                else {
                    if ($retryCount -ge 10){
                        "Failed to dump MailboxAuditLog for $($userId) $($retryCount + 1) times - aborting" | Write-Log -LogPath $logFile -LogLevel "Error"
                        $countMailboxAuditLogEntries = 0
                        $stopLoop = $true
                    }
                    else {
                        $errorMessage = $_.Exception.Message
                        "Failed to dump MailboxAuditLog for $($userId) $($retryCount + 1) times - reconnecting, sleeping and retrying - $($errorMessage)" | Write-Log -LogPath $logFile -LogLevel "Warning"
                        Connect-ExchangeOnlineApplication -logFile $logFile -certificate $cert -appId $appId -organization $tenant
                        Start-Sleep -Seconds (60 * ($retryCount + 1))
                        $retryCount = $retryCount + 1
                    }
                }
            }
        } while ($stopLoop -eq $false)

        if ($countMailboxAuditLogEntries -gt 250000){
            "More than 250000 events in one day, consider those events incomplete between $($startDate) and $($endDate) for $($userId)" | Write-Log -LogPath $logFile -LogLevel "Warning"
        }
        if ($countMailboxAuditLogEntries -gt 0){
            "Collected $countMailboxAuditLogEntries MailboxAuditLog events for $startDate - $endDate for user $userId" | Write-Log -LogPath $logFile -LogLevel "Info"
            $mailboxAuditLogEntries | ConvertTo-Json -Depth 99 | Out-File $outputFile -Encoding UTF8 -Append
            $mailboxAuditLogEntries = @()
            $countMailboxAuditLogEntries = 0
        }
        else {
            "0 MailboxAuditLog events between {0:yyyy-MM-dd} {0:HH:mm:ss} and {1:yyyy-MM-dd} {1:HH:mm:ss} were found for user $userId" -f ($startDate, $endDate) | Write-Log -LogPath $logFile -LogLevel "Warning"
        }
    }
}