Scripts/Get-AzureADGraphLogs.ps1

function Get-GraphEntraSignInLogs {
    <#
    .SYNOPSIS
    Gets of sign-ins logs.
 
    .DESCRIPTION
    The Get-GraphEntraSignInLogs GraphAPI cmdlet collects the contents of the Azure Active Directory sign-in logs.
 
    .PARAMETER startDate
    The startDate parameter specifies the date from which all logs need to be collected.
 
    .PARAMETER endDate
    The Before parameter specifies the date endDate which all logs need to be collected.
 
    .PARAMETER MergeOutput
    MergeOutput is the parameter specifying if you wish to merge outputs to a single file
    Default: No
 
    .PARAMETER Output
    Output is the parameter specifying the JSON or SOF-ELK output type. The SOF-ELK output can be imported into the platform of the same name.
    Default: JSON
 
    .PARAMETER LogLevel
    Specifies the level of logging:
    None: No logging
    Minimal: Critical errors only
    Standard: Normal operational logging
    Default: Standard
 
    .PARAMETER OutputDir
    outputDir is the parameter specifying the output directory.
    Default: The output will be written to: Output\EntraID\{date_SignInLogs}\SignInLogs.json
 
    .PARAMETER Encoding
    Encoding is the parameter specifying the encoding of the JSON output file.
    Default: UTF8
 
    .PARAMETER UserIds
    UserIds is the UserIds parameter filtering the log entries by the account of the user who performed the actions.
 
    .EXAMPLE
    Get-GraphEntraSignInLogs
    Get all audit logs of sign-ins.
 
    .EXAMPLE
    Get-GraphEntraSignInLogs -Application
    Get all audit logs of sign-ins via application authentication.
 
    .EXAMPLE
    Get-GraphEntraSignInLogs -endDate 2024-04-12
    Get audit logs before 2024-04-12.
 
    .EXAMPLE
    Get-GraphEntraSignInLogs -startDate 2024-04-12
    Get audit logs after 2024-04-12.
 
    .EXAMPLE
    Get-GraphEntraSignInLogs -Output SOF-ELK -MergeOutput
    Get the Azure Active Directory SignIn Log in a format compatible with the SOF-ELK platform and merge all data into a single file.
#>

    [CmdletBinding()]
    param(
        [string]$startDate,
        [string]$endDate,
        [string]$Output = "JSON",
        [string]$OutputDir,
        [string]$UserIds,
        [switch]$MergeOutput,
        [string]$Encoding = "UTF8",
        [ValidateSet('None', 'Minimal', 'Standard')]
        [string]$LogLevel = 'Standard'
    )

    Set-LogLevel -Level ([LogLevel]::$LogLevel)
    $summary = @{
        TotalRecords = 0
        StartTime = Get-Date
        ProcessingTime = $null
        TotalFiles = 0
    }

    Write-LogFile -Message "=== Starting Sign-in Log Collection ===" -Color "Cyan" -Level Minimal
    $requiredScopes = @("AuditLog.Read.All", "Directory.Read.All")
    $graphAuth = Get-GraphAuthType -RequiredScopes $RequiredScopes

    $date = [datetime]::Now.ToString('yyyyMMdd') 
    if ($OutputDir -eq "" ){
        $OutputDir = "Output\EntraID\$($date)-SignInLogs"
        if (!(test-path $OutputDir)) {
            New-Item -ItemType Directory -Force -path $OutputDir > $null
        }
    }
    else {
        if (!(Test-Path -Path $OutputDir)) {
            Write-LogFile -Message "[Error] Custom directory invalid: $OutputDir" -Level Minimal -Color "Red"
            return
        }
    }

    StartDateAz -Quiet
    EndDate -Quiet

    $StartDate = $script:StartDate.ToString('yyyy-MM-ddTHH:mm:ssZ')
    $EndDate = $script:EndDate.ToString('yyyy-MM-ddTHH:mm:ssZ')

    Write-LogFile -Message "Start Date: $StartDate" -Level Standard
    Write-LogFile -Message "End Date: $EndDate" -Level Standard
    Write-LogFile -Message "Output Format: $Output" -Level Standard
    Write-LogFile -Message "Output Directory: $OutputDir" -Level Standard
    if ($UserIds) {
        Write-LogFile -Message "Filtering for User: $UserIds" -Level Standard
    }
    Write-LogFile -Message "----------------------------------------`n" -Level Standard

    $filterQuery = "createdDateTime ge $StartDate and createdDateTime le $EndDate"
    if ($UserIds) {
        $filterQuery += " and startsWith(userPrincipalName, '$UserIds')"
    }

    $encodedFilterQuery = [System.Web.HttpUtility]::UrlEncode($filterQuery)
    $apiUrl = "https://graph.microsoft.com/beta/auditLogs/signIns?`$filter=$encodedFilterQuery"

    try {
        Do {
            $retryCount = 0
            $maxRetries = 3
            $success = $false

            while (-not $success -and $retryCount -lt $maxRetries) {
                try {
                    $response = Invoke-MgGraphRequest -Uri $apiUrl -Method Get -ContentType "application/json; odata.metadata=minimal; odata.streaming=true;" -OutputType Json
                    $responseJson = $response | ConvertFrom-Json 
                    $success = $true
                }
                catch {
                    $retryCount++
                    if ($retryCount -lt $maxRetries) {
                        Write-LogFile -Message "[WARNING] Failed to acquire logs. Retrying... Attempt $retryCount of $maxRetries" -Level Standard -Color "Yellow"
                        Start-Sleep -Seconds 15
                    }
                    else {
                        Write-LogFile -Message "[ERROR] Failed to acquire logs after $maxRetries attempts. Error: $($_.Exception.Message)" -Level Minimal -Color "Red"
                        throw
                    }
                }
            }
           
            if ($responseJson.value) {
                $date = [datetime]::Now.ToString('yyyyMMddHHmmss') 
                $filePath = Join-Path -Path $OutputDir -ChildPath "$($date)-SignInLogs.json"

                if ($Output -eq "JSON" ) {
                    $responseJson.value | ConvertTo-Json -Depth 100 | Out-File -FilePath $filePath -Append -Encoding $Encoding    
                } 
                elseif ($Output -eq "SOF-ELK"){
                    # UTF8 is fixed, as it is required by SOF-ELK
                    foreach ($item in $responseJson.value) {
                        $item | ConvertTo-Json -Depth 100 -Compress | Out-File -FilePath $filePath -Append -Encoding UTF8    
                    }
                }

                $currentBatchCount = ($responseJson.value | Measure-Object).Count
                $summary.TotalRecords += $currentBatchCount
                $summary.TotalFiles++

                $dates = $responseJson.value | ForEach-Object {
                    [DateTime]::Parse($_.CreatedDateTime, [System.Globalization.CultureInfo]::InvariantCulture)
                } | Sort-Object
                
                $from =  $dates | Select-Object -First 1
                $to = ($dates | Select-Object -Last 1)
                Write-LogFile -Message "[INFO] Retrieved $currentBatchCount records between $from and $to" -Level Standard -Color "Green"
            }
            $apiUrl = $responseJson.'@odata.nextLink'
        } While ($apiUrl)
        
        if ($Output -eq "JSON" -and ($MergeOutput.IsPresent)) {
            Write-LogFile -Message "[INFO] Merging output files into one file"
            Merge-OutputFiles -OutputDir $OutputDir -OutputType "JSON" -MergedFileName "SignInLogs-Combined.json"
        }

        elseif ($Output -eq "SOF-ELK" -and ($MergeOutput.IsPresent)) {
            Write-LogFile -Message "[INFO] Merging output files into one file"
            Merge-OutputFiles -OutputDir $OutputDir -OutputType "SOF-ELK" -MergedFileName "SignInLogs-Combined.json"
        }

        $summary.ProcessingTime = (Get-Date) - $summary.StartTime
        Write-LogFile -Message "`nCollection Summary:" -Color "Cyan" -Level Standard
        Write-LogFile -Message " Total Records: $($summary.TotalRecords)" -Level Standard
        Write-LogFile -Message " Files Created: $($summary.TotalFiles)" -Level Standard
        Write-LogFile -Message " Output Directory: $OutputDir" -Level Standard
        Write-LogFile -Message " Processing Time: $($summary.ProcessingTime.ToString('mm\:ss'))" -Level Standard -Color "Green"
    }
    
    catch {
        Write-LogFile -Message "[ERROR] An error occurred: $($_.Exception.Message)" -Level Minimal -Color "Red"
        throw
    }
}

function Get-GraphEntraAuditLogs {
    <#
    .SYNOPSIS
    Get directory audit logs.
 
    .DESCRIPTION
    The Get-GraphEntraAuditLogs GraphAPI cmdlet to collect the contents of the Azure Active Directory Audit logs.
 
    .PARAMETER startDate
    The startDate parameter specifies the date from which all logs need to be collected.
 
    .PARAMETER endDate
    The Before parameter specifies the date endDate which all logs need to be collected.
 
    .PARAMETER OutputDir
    outputDir is the parameter specifying the output directory.
    Default: The output will be written to: "Output\EntraID\{date_AuditLogs}\Auditlogs.json
 
    .PARAMETER UserIds
    UserIds is the UserIds parameter filtering the log entries by the account of the user who performed the actions.
 
    .PARAMETER All
    When specified along with UserIds, this parameter filters the results to include events where the provided UserIds match any user principal name found in either the userPrincipalNames or targetResources fields.
 
    .PARAMETER Encoding
    Encoding is the parameter specifying the encoding of the JSON output file.
    Default: UTF8
 
    .PARAMETER MergeOutput
    MergeOutput is the parameter specifying if you wish to merge outputs to a single file
    Default: No
 
    .PARAMETER Output
    Output is the parameter specifying the JSON or SOF-ELK output type. The SOF-ELK output can be imported into the platform of the same name.
    Default: JSON
 
    .EXAMPLE
    Get-GraphEntraAuditLogs
    Get directory audit logs.
 
    .EXAMPLE
    Get-GraphEntraAuditLogs -Application
    Get directory audit logs via application authentication.
 
    .PARAMETER LogLevel
    Specifies the level of logging:
    None: No logging
    Minimal: Critical errors only
    Standard: Normal operational logging
    Default: Standard
 
    .EXAMPLE
    Get-GraphEntraAuditLogs -UserIds 'user@example.com' -All
    Get sign-in logs for 'user@example.com', including both userPrincipalName and targetResources in the filter.
 
    .EXAMPLE
    Get-GraphEntraAuditLogs -Before 2024-04-12
    Get directory audit logs before 2024-04-12.
 
    .EXAMPLE
    Get-GraphEntraAuditLogs -After 2024-04-12
    Get directory audit logs after 2024-04-12.
    #>

    [CmdletBinding()]
    param(
        [string]$startDate,
        [string]$endDate,
        [string]$OutputDir,
        [string]$Output = "JSON",
        [string]$Encoding = "UTF8",
        [switch]$MergeOutput,
        [string]$UserIds,
        [switch]$All,
        [ValidateSet('None', 'Minimal', 'Standard')]
        [string]$LogLevel = 'Standard'
    )

    Set-LogLevel -Level ([LogLevel]::$LogLevel)
    $summary = @{
        TotalRecords = 0
        StartTime = Get-Date
        ProcessingTime = $null
        TotalFiles = 0
    }

    Write-LogFile -Message "=== Starting Sign-in Log Collection ===" -Color "Cyan" -Level Minimal
    $requiredScopes = @("AuditLog.Read.All", "Directory.Read.All")
    $graphAuth = Get-GraphAuthType -RequiredScopes $RequiredScopes
    
    $date = [datetime]::Now.ToString('yyyyMMdd') 
    if ($OutputDir -eq "" ){
        $OutputDir = "Output\EntraID\$($date)-Auditlogs"
        if (!(test-path $OutputDir)) {
            New-Item -ItemType Directory -Force -Name $OutputDir > $null
            write-logFile -Message "[INFO] Creating the following directory: $OutputDir"
        }
    }
    else {
        if (!(Test-Path -Path $OutputDir)) {
            Write-LogFile -Message "[Error] Custom directory invalid: $OutputDir" -Level Minimal -Color "Red"
            return
        }
    }

    StartDateAz -Quiet
    EndDate -Quiet

    $StartDate = $script:StartDate.ToString('yyyy-MM-ddTHH:mm:ssZ')
    $EndDate = $script:EndDate.ToString('yyyy-MM-ddTHH:mm:ssZ')

    Write-LogFile -Message "Start Date: $StartDate" -Level Standard
    Write-LogFile -Message "End Date: $EndDate" -Level Standard
    Write-LogFile -Message "Output Format: $Output" -Level Standard
    Write-LogFile -Message "Output Directory: $OutputDir" -Level Standard
    if ($UserIds) {
        Write-LogFile -Message "Filtering for User: $UserIds" -Level Standard
    }
    Write-LogFile -Message "----------------------------------------`n" -Level Standard

    $filterQuery = "activityDateTime ge $StartDate and activityDateTime le $EndDate"
    if ($UserIds) {
        $filterQuery += " and startsWith(initiatedBy/user/userPrincipalName, '$UserIds')"

        if ($All.IsPresent) {
            $filterQuery = "($filterQuery) or (targetResources/any(tr: tr/userPrincipalName eq '$UserIds'))"
        }
    }
    else {
        if ($All.IsPresent) {
            Write-LogFile -Message "[WARNING] '-All' switch has no effect without specifying UserIds"
        }
    }

    $encodedFilterQuery = [System.Web.HttpUtility]::UrlEncode($filterQuery)
    $apiUrl = "https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?`$filter=$encodedFilterQuery"

    try {
        Do {
            $retryCount = 0
            $maxRetries = 3
            $success = $false

            while (-not $success -and $retryCount -lt $maxRetries) {
                try { 
                    $response = Invoke-MgGraphRequest -Uri $apiUrl -Method Get -ContentType "application/json; odata.metadata=minimal; odata.streaming=true;" -OutputType Json
                    $responseJson = $response | ConvertFrom-Json 
                    $success = $true
                }
                catch {
                    $retryCount++
                    if ($retryCount -lt $maxRetries) {
                        Write-LogFile -Message "[WARNING] Failed to acquire logs. Retrying... Attempt $retryCount of $maxRetries" -Level Standard -Color "Yellow"
                        Start-Sleep -Seconds 15
                    }
                    else {
                        Write-LogFile -Message "[ERROR] Failed to acquire logs after $maxRetries attempts. Error: $($_.Exception.Message)" -Level Minimal -Color "Red"
                        throw
                    }
                }
            }

            if ($responseJson.value) {
                $date = [datetime]::Now.ToString('yyyyMMddHHmmss') 
                $filePath = Join-Path -Path $OutputDir -ChildPath "$($date)-AuditLogs.json"

                if ($Output -eq "JSON") {
                    $responseJson.value | ConvertTo-Json -Depth 100 | Out-File -FilePath $filePath -Append -Encoding $Encoding
                }
                elseif ($Output -eq "SOF-ELK") {
                    # UTF8 is fixed, as it is required by SOF-ELK
                    foreach ($item in $responseJson.value) {
                        $item | ConvertTo-Json -Depth 100 -Compress | Out-File -FilePath $filePath -Append -Encoding UTF8
                    }
                }

                $currentBatchCount = ($responseJson.value | Measure-Object).Count
                $summary.TotalRecords += $currentBatchCount
                $summary.TotalFiles++
                
                $dates = $responseJson.value | ForEach-Object {
                    [DateTime]::Parse($_.activityDateTime, [System.Globalization.CultureInfo]::InvariantCulture)
                } | Sort-Object

                $from =  $dates | Select-Object -First 1
                $fromstr =  $from.ToString('yyyy-MM-ddTHH:mmZ')
                $to = ($dates | Select-Object -Last 1).ToString('yyyy-MM-ddTHH:mmZ')
                Write-LogFile -Message "[INFO] Retrieved $currentBatchCount records between $fromstr and $to" -Level Standard -Color "Green"
            }
            $apiUrl = $responseJson.'@odata.nextLink'
        } While ($apiUrl)

        if ($Output -eq "JSON" -and ($MergeOutput.IsPresent)) {
            Merge-OutputFiles -OutputDir $OutputDir -OutputType "JSON" -MergedFileName "AuditLogs-Combined.json"
        }
        elseif ($Output -eq "SOF-ELK" -and ($MergeOutput.IsPresent)) {
            Merge-OutputFiles -OutputDir $OutputDir -OutputType "SOF-ELK" -MergedFileName "AuditLogs-Combined.json"
        }

        $summary.ProcessingTime = (Get-Date) - $summary.StartTime
        Write-LogFile -Message "`nCollection Summary:" -Color "Cyan" -Level Standard
        Write-LogFile -Message " Total Records: $($summary.TotalRecords)" -Level Standard
        Write-LogFile -Message " Files Created: $($summary.TotalFiles)" -Level Standard
        Write-LogFile -Message " Output Directory: $OutputDir" -Level Standard
        Write-LogFile -Message " Processing Time: $($summary.ProcessingTime.ToString('mm\:ss'))" -Level Standard -Color "Green"
    }
    catch {
        Write-logFile -Message "[ERROR] An error occurred: $($_.Exception.Message)" -Color "Red" -Level Minimal
        throw
    }
}