Scripts/TriageFunctions.ps1

function Get-EntraApplicationsForSpecificUsers {
    <#
    .SYNOPSIS
    Retrieves Entra ID applications owned by or assigned to specific users.
 
    .DESCRIPTION
    This function efficiently collects information about applications that are owned by
    or assigned to the specified users, avoiding the need to process all applications
    in the tenant.
 
    .PARAMETER OutputDir
    OutputDir is the parameter specifying the output directory.
    Default: Output\Applications
 
    .PARAMETER Encoding
    Encoding is the parameter specifying the encoding of the CSV output file.
    Default: UTF8
 
    .PARAMETER LogLevel
    Specifies the level of logging:
    None: No logging
    Minimal: Critical errors only
    Standard: Normal operational logging
    Debug: Verbose logging for debugging purposes
    Default: Standard
 
    .PARAMETER UserIds
    Required parameter to filter applications by owner or assignments.
    Only shows applications owned by or assigned to these users.
 
    .EXAMPLE
    Get-EntraApplications -UserIds @("admin@domain.com")
    Retrieves applications owned by or assigned to specific users.
 
    .EXAMPLE
    Get-EntraApplications -UserIds @("user1@domain.com","user2@domain.com") -OutputDir "C:\Security\Apps"
    Retrieves applications for multiple users with custom output directory.
    #>


    [CmdletBinding()]
    param(
        [string]$OutputDir = "Output\Applications", 
        [string]$Encoding = "UTF8",
        [ValidateSet('None', 'Minimal', 'Standard', 'Debug')]
        [string]$LogLevel = 'Standard',
        [Parameter(Mandatory=$true)]
        [string[]]$UserIds
    )

    Set-LogLevel -Level ([LogLevel]::$LogLevel)
    $isDebugEnabled = $script:LogLevel -eq [LogLevel]::Debug

    if ($isDebugEnabled) {
        Write-LogFile -Message "[DEBUG] PowerShell Version: $($PSVersionTable.PSVersion)" -Level Debug
        Write-LogFile -Message "[DEBUG] Input parameters:" -Level Debug
        Write-LogFile -Message "[DEBUG] OutputDir: '$OutputDir'" -Level Debug
        Write-LogFile -Message "[DEBUG] Encoding: '$Encoding'" -Level Debug
        Write-LogFile -Message "[DEBUG] UserIds: '$($UserIds -join ', ')'" -Level Debug
        Write-LogFile -Message "[DEBUG] LogLevel: '$LogLevel'" -Level Debug
    }

    Write-LogFile -Message "=== Starting Entra Applications Collection ===" -Color "Cyan" -Level Standard

    $requiredScopes = @("Application.Read.All", "Directory.Read.All")
    $graphAuth = Get-GraphAuthType -RequiredScopes $RequiredScopes

    if ($isDebugEnabled) {
        Write-LogFile -Message "[DEBUG] Graph authentication details:" -Level Debug
        Write-LogFile -Message "[DEBUG] Required scopes: $($requiredScopes -join ', ')" -Level Debug
        Write-LogFile -Message "[DEBUG] Authentication type: $($graphAuth.AuthType)" -Level Debug
        Write-LogFile -Message "[DEBUG] Current scopes: $($graphAuth.Scopes -join ', ')" -Level Debug
    }

    if (!(Test-Path $OutputDir)) {
        New-Item -ItemType Directory -Force -Path $OutputDir > $null
        Write-LogFile -Message "[INFO] Created output directory: $OutputDir" -Level Standard
    }

    $validUsers = @()
    Write-LogFile -Message "[INFO] Resolving $($UserIds.Count) users..." -Level Standard
    foreach ($userId in $UserIds) {
        try {
            $user = Get-MgUser -UserId $userId -ErrorAction Stop
            $validUsers += $user
            Write-LogFile -Message "[INFO] Resolved user: $($user.UserPrincipalName)" -Level Standard
        }
        catch {
            Write-LogFile -Message "[WARNING] Could not resolve user: $userId" -Color "Yellow" -Level Minimal
        }
    }

    if ($validUsers.Count -eq 0) {
        Write-LogFile -Message "[ERROR] No valid users found. Cannot proceed." -Color "Red" -Level Minimal
        return
    }

    $results = @()
    $processedAppIds = @{}
    $summary = @{
        OwnedApps = 0
        AssignedApps = 0
        TotalApps = 0
        StartTime = Get-Date
        ProcessingTime = $null
    }

    foreach ($user in $validUsers) {
        Write-LogFile -Message "[INFO] Processing user: $($user.UserPrincipalName)" -Level Standard

        # Get owned applications
        try {
            $ownedApps = Get-MgUserOwnedObject -UserId $user.Id -All | Where-Object { $_.AdditionalProperties.'@odata.type' -eq '#microsoft.graph.application' }
            
            foreach ($ownedAppRef in $ownedApps) {
                if (-not $processedAppIds.ContainsKey($ownedAppRef.Id)) {
                    try {
                        $app = Get-MgApplication -ApplicationId $ownedAppRef.Id
                        $processedAppIds[$ownedAppRef.Id] = $true
                        $summary.OwnedApps++
                        
                        # Get service principal if it exists
                        $servicePrincipal = $null
                        try {
                            $servicePrincipals = Get-MgServicePrincipal -Filter "appId eq '$($app.AppId)'"
                            $servicePrincipal = $servicePrincipals | Select-Object -First 1
                        }
                        catch { }

                        $appObject = [PSCustomObject]@{
                            AssociationType = "Owner"
                            AssociatedUser = $user.UserPrincipalName
                            ApplicationName = $app.DisplayName
                            ApplicationId = $app.AppId
                            ObjectId = $app.Id
                            PublisherName = if ($servicePrincipal) { $servicePrincipal.PublisherName } else { "" }
                            ApplicationType = if ($servicePrincipal) { 
                                $types = @()
                                if ($servicePrincipal.AppOwnerOrganizationId -eq "f8cdef31-a31e-4b4a-93e4-5f571e91255a" -or $servicePrincipal.AppOwnerOrganizationId -eq "72f988bf-86f1-41af-91ab-2d7cd011db47") { 
                                    $types += "Microsoft Application" 
                                }
                                if ($servicePrincipal.ServicePrincipalType -eq "ManagedIdentity") { 
                                    $types += "Managed Identity" 
                                }
                                if ($servicePrincipal.Tags -contains "WindowsAzureActiveDirectoryIntegratedApp") { 
                                    $types += "Enterprise Application" 
                                }
                                if ($types.Count -eq 0) { "Internal Application" } else { $types -join " & " }
                            } else { "Internal Application" }
                            CreatedDateTime = $app.CreatedDateTime
                            ServicePrincipalEnabled = if ($servicePrincipal) { $servicePrincipal.AccountEnabled } else { "N/A" }
                            HasClientSecrets = ($app.PasswordCredentials -and $app.PasswordCredentials.Count -gt 0)
                            HasCertificates = ($app.KeyCredentials -and $app.KeyCredentials.Count -gt 0)
                            RequiredApiPermissionCount = if ($app.RequiredResourceAccess) { 
                                ($app.RequiredResourceAccess | ForEach-Object { $_.ResourceAccess.Count } | Measure-Object -Sum).Sum 
                            } else { 0 }
                            SignInAudience = $app.SignInAudience
                            Homepage = if ($servicePrincipal) { $servicePrincipal.Homepage } else { $app.Web.HomePageUrl }
                            WebRedirectUris = ($app.Web.RedirectUris -join "; ")
                            PublicClientRedirectUris = ($app.PublicClient.RedirectUris -join "; ")
                        }
                        
                        $results += $appObject
                    }
                    catch {
                        Write-LogFile -Message "[WARNING] Could not process owned app: $($_.Exception.Message)" -Color "Yellow" -Level Minimal
                    }
                }
            }
        }
        catch {
            Write-LogFile -Message "[WARNING] Error getting owned apps for $($user.UserPrincipalName): $($_.Exception.Message)" -Color "Yellow" -Level Minimal
        }

        # Get application assignments
        try {
            $userAssignments = Get-MgUserAppRoleAssignment -UserId $user.Id -All
            
            foreach ($assignment in $userAssignments) {
                try {
                    $servicePrincipal = Get-MgServicePrincipal -ServicePrincipalId $assignment.ResourceId
                    
                    $appKey = "SP_$($servicePrincipal.Id)"
                    
                    if (-not $processedAppIds.ContainsKey($appKey)) {
                        $processedAppIds[$appKey] = $true
                        $summary.AssignedApps++
                        
                        # Try to get the corresponding application registration
                        $app = $null
                        if ($servicePrincipal.AppId) {
                            try {
                                $apps = Get-MgApplication -Filter "appId eq '$($servicePrincipal.AppId)'"
                                $app = $apps | Select-Object -First 1
                            }
                            catch { }
                        }

                        $appObject = [PSCustomObject]@{
                            AssociationType = "Assignment"
                            AssociatedUser = $user.UserPrincipalName
                            ApplicationName = $servicePrincipal.DisplayName
                            ApplicationId = $servicePrincipal.AppId
                            ObjectId = if ($app) { $app.Id } else { $servicePrincipal.Id }
                            PublisherName = $servicePrincipal.PublisherName
                            ApplicationType = if ($servicePrincipal.AppOwnerOrganizationId -eq "f8cdef31-a31e-4b4a-93e4-5f571e91255a" -or $servicePrincipal.AppOwnerOrganizationId -eq "72f988bf-86f1-41af-91ab-2d7cd011db47") { 
                                "Microsoft Application" 
                            } elseif ($servicePrincipal.ServicePrincipalType -eq "ManagedIdentity") { 
                                "Managed Identity" 
                            } elseif ($servicePrincipal.Tags -contains "WindowsAzureActiveDirectoryIntegratedApp") { 
                                "Enterprise Application" 
                            } else { 
                                "Internal Application" 
                            }
                            CreatedDateTime = if ($app) { $app.CreatedDateTime } else { $servicePrincipal.AdditionalProperties.createdDateTime }
                            ServicePrincipalEnabled = $servicePrincipal.AccountEnabled
                            HasClientSecrets = if ($app) { ($app.PasswordCredentials -and $app.PasswordCredentials.Count -gt 0) } else { "N/A" }
                            HasCertificates = if ($app) { ($app.KeyCredentials -and $app.KeyCredentials.Count -gt 0) } else { "N/A" }
                            RequiredApiPermissionCount = if ($app -and $app.RequiredResourceAccess) { 
                                ($app.RequiredResourceAccess | ForEach-Object { $_.ResourceAccess.Count } | Measure-Object -Sum).Sum 
                            } else { "N/A" }
                            SignInAudience = if ($app) { $app.SignInAudience } else { "" }
                            Homepage = $servicePrincipal.Homepage
                            WebRedirectUris = if ($app) { ($app.Web.RedirectUris -join "; ") } else { "" }
                            PublicClientRedirectUris = if ($app) { ($app.PublicClient.RedirectUris -join "; ") } else { "" }
                        }
                        
                        $results += $appObject
                    }
                }
                catch {
                    Write-LogFile -Message "[WARNING] Could not process assignment: $($_.Exception.Message)" -Color "Yellow" -Level Minimal
                }
            }
        }
        catch {
            Write-LogFile -Message "[WARNING] Error getting assignments for $($user.UserPrincipalName): $($_.Exception.Message)" -Color "Yellow" -Level Minimal
        }
    }

    $summary.TotalApps = $results.Count
    $summary.ProcessingTime = (Get-Date) - $summary.StartTime

    $date = Get-Date -Format "yyyyMMddHHmm"
    $outputPath = Join-Path $OutputDir "$($date)-UserApplications.csv"
    
    Write-LogFile -Message "[INFO] Exporting $($results.Count) applications to CSV..." -Level Standard
    $results | Export-Csv -Path $outputPath -NoTypeInformation -Encoding $Encoding

    Write-LogFile -Message "`n=== User Applications Summary ===" -Color "Cyan" -Level Standard
    Write-LogFile -Message "Users Processed: $($validUsers.Count)" -Level Standard
    Write-LogFile -Message "Owned Applications: $($summary.OwnedApps)" -Level Standard
    Write-LogFile -Message "Assigned Applications: $($summary.AssignedApps)" -Level Standard
    Write-LogFile -Message "Total Applications: $($summary.TotalApps)" -Level Standard
    Write-LogFile -Message "Output File: $outputPath" -Level Standard
    Write-LogFile -Message "Processing Time: $($summary.ProcessingTime.ToString('mm\:ss'))" -Color "Green" -Level Standard
    Write-LogFile -Message "===================================" -Color "Cyan" -Level Standard
}

function Get-QuickUALOperations {
<#
    .SYNOPSIS
    Quickly retrieves specific operations from Unified Audit Log for triage purposes.
 
    .DESCRIPTION
    A lightweight function designed for quick security triage that focuses on specific
    operations in the UAL without the complexity of the full Get-UAL function.
    Optimized for speed and simplicity.
 
    .PARAMETER Operations
    Array of specific operations to search for (e.g., 'SearchQueryInitiated', 'MailItemsAccessed')
     
    .PARAMETER UserIds
    Comma-separated list of user IDs to filter on
     
    .PARAMETER StartDate
    Start date for the search (defaults to 7 days ago)
     
    .PARAMETER EndDate
    End date for the search (defaults to now)
     
    .PARAMETER OutputDir
    Output directory for results
     
    .PARAMETER MaxResults
    Maximum number of results to retrieve per operation (default: 5000)
     
    .PARAMETER LogLevel
    Logging level
     
    .EXAMPLE
    Get-QuickUALOperations -Operations @('SearchQueryInitiated', 'MailItemsAccessed') -UserIds "user@domain.com"
     
    .EXAMPLE
    Get-QuickUALOperations -Operations @('New-InboxRule', 'Set-InboxRule') -OutputDir "C:\Triage\Case123"
#>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string[]]$Operations,
        [string[]]$UserIds,
        [string]$StartDate,
        [string]$EndDate,
        [string]$OutputDir,
        [ValidateSet("CSV", "JSON", "SOF-ELK")]
        [string]$Output = "CSV",
        [int]$MaxResults = 5000,
        [ValidateSet('None', 'Minimal', 'Standard', 'Debug')]
        [string]$LogLevel = 'Standard'
    )

    Set-LogLevel -Level ([LogLevel]::$LogLevel)
    $WarningPreference = 'SilentlyContinue'

    StartDate -Quiet
    EndDate -Quiet
    
    if ([string]::IsNullOrEmpty($OutputDir)) {
        $timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
        $OutputDir = "Output\QuickUAL\$timestamp"
    }
    
    if (!(Test-Path $OutputDir)) {
        New-Item -ItemType Directory -Force -Path $OutputDir > $null
        Write-LogFile -Message "[INFO] Created output directory: $OutputDir" -Level Standard
    }

    Write-LogFile -Message "=== Quick UAL Operations Collection ===" -Color "Cyan" -Level Standard
    Write-LogFile -Message "Operations: $($Operations -join ', ')" -Level Standard
    Write-LogFile -Message "Date Range: $($script:StartDate.ToString('yyyy-MM-dd HH:mm:ss')) to $($script:EndDate.ToString('yyyy-MM-dd HH:mm:ss'))" -Level Standard
    if ($UserIds) {
        Write-LogFile -Message "Target Users: $($UserIds -join ', ')" -Level Standard
    }
    Write-LogFile -Message "Output Format: $Output" -Level Standard
    Write-LogFile -Message "Max Results per Operation: $MaxResults" -Level Standard
    Write-LogFile -Message "Output Directory: $OutputDir" -Level Standard
    Write-LogFile -Message "----------------------------------------" -Level Standard

    $allResults = @()
    $totalRecords = 0

    foreach ($operation in $Operations) {
        Write-LogFile -Message "[INFO] Searching for operation: $operation" -Level Minimal
        
        try {
            $searchParams = @{
                StartDate = $script:StartDate
                EndDate = $script:EndDate
                Operations = $operation
            }
            
            if ($UserIds -and $UserIds.Count -gt 0) {
                $searchParams.UserIds = $UserIds
            }
            
            $countResult = Search-UnifiedAuditLog @searchParams -ResultSize 1 -WarningAction SilentlyContinue | Select-Object -First 1 -ExpandProperty ResultCount
            
            if ($null -eq $countResult -or $countResult -eq 0) {
                Write-LogFile -Message "[INFO] No records found for operation: $operation" -Level Standard -Color "Yellow"
                continue
            }
            
            Write-LogFile -Message "[INFO] Found $countResult records for operation: $operation" -Level Standard -Color "Green"
            
            if ($countResult -gt $MaxResults) {
                Write-LogFile -Message "[WARNING] Found $countResult records but the max is $MaxResults. Consider using Get-UAL to get all results available if needed." -Color "Yellow" -Level Minimal
            }
            
            $results = Search-UnifiedAuditLog @searchParams -ResultSize $MaxResults -WarningAction SilentlyContinue
 
            if ($results) {
                $processedResults = $results | ForEach-Object {
                    $record = $_ | Select-Object *
                    if ($record.AuditData) {
                        try {
                            $record.AuditData = $record.AuditData | ConvertFrom-Json
                        }
                        catch {
                            Write-LogFile -Message "[WARNING] Failed to parse AuditData for record: $($record.Identity)" -Color "Yellow" -Level Standard
                        }
                    }
                    $record.PSObject.Properties.Add((New-Object PSNoteProperty('OperationQueried', $operation)))
                    $record
                }
                
                $allResults += $processedResults
                $totalRecords += $processedResults.Count
                $operationFileName = $operation -replace '[\\/:*?"<>|]', '_' 
                
                # Save as JSON
                $jsonPath = Join-Path $OutputDir "$operationFileName.json"
                $processedResults | ConvertTo-Json -Depth 10 | Out-File $jsonPath -Encoding UTF8
                Write-LogFile -Message "[INFO] Saved $($processedResults.Count) records to: $jsonPath" -Level Standard
                
                # Save as CSV (flatten AuditData for CSV)
                $csvPath = Join-Path $OutputDir "$operationFileName.csv"
                $csvResults = $processedResults | ForEach-Object {
                    $flatRecord = $_ | Select-Object * -ExcludeProperty AuditData
                    
                    # Add key AuditData fields as separate columns
                    if ($_.AuditData) {
                        $flatRecord | Add-Member -NotePropertyName "AuditData_UserId" -NotePropertyValue $_.AuditData.UserId -Force
                        $flatRecord | Add-Member -NotePropertyName "AuditData_ClientIP" -NotePropertyValue $_.AuditData.ClientIP -Force
                        $flatRecord | Add-Member -NotePropertyName "AuditData_UserAgent" -NotePropertyValue $_.AuditData.UserAgent -Force
                        $flatRecord | Add-Member -NotePropertyName "AuditData_ObjectId" -NotePropertyValue $_.AuditData.ObjectId -Force
                        
                        $flatRecord | Add-Member -NotePropertyName "AuditData_Raw" -NotePropertyValue ($_.AuditData | ConvertTo-Json -Compress -Depth 5) -Force
                    }
                    $flatRecord
                }
                
                $csvResults | Export-Csv $csvPath -NoTypeInformation -Encoding UTF8
                Write-LogFile -Message "[INFO] Saved CSV format to: $csvPath" -Level Standard
            }
        }
        catch {
            Write-LogFile -Message "[ERROR] Failed to retrieve operation '$operation': $($_.Exception.Message)" -Color "Red" -Level Minimal
        }
    }

    if ( $allResults.Count -gt 0) {
        Write-LogFile -Message "[INFO] Creating combined file with all operations..." -Level Standard
        
        switch ($Output) {
            "CSV" {
                $combinedCsvPath = Join-Path $OutputDir "UAL-Operations-Combined.csv"
                $combinedCsvResults = $allResults | ForEach-Object {
                    $flatRecord = $_ | Select-Object * -ExcludeProperty AuditData
                    
                    if ($_.AuditData) {
                        $flatRecord | Add-Member -NotePropertyName "AuditData_UserId" -NotePropertyValue $_.AuditData.UserId -Force
                        $flatRecord | Add-Member -NotePropertyName "AuditData_ClientIP" -NotePropertyValue $_.AuditData.ClientIP -Force
                        $flatRecord | Add-Member -NotePropertyName "AuditData_UserAgent" -NotePropertyValue $_.AuditData.UserAgent -Force
                        $flatRecord | Add-Member -NotePropertyName "AuditData_ObjectId" -NotePropertyValue $_.AuditData.ObjectId -Force
                        $flatRecord | Add-Member -NotePropertyName "AuditData_Raw" -NotePropertyValue ($_.AuditData | ConvertTo-Json -Compress -Depth 5) -Force
                    }
                    $flatRecord
                }
                $combinedCsvResults | Export-Csv $combinedCsvPath -NoTypeInformation -Encoding $Encoding
                Write-LogFile -Message "[INFO] Saved combined CSV file: $combinedCsvPath" -Level Standard -Color "Green"
            }
            "JSON" {
                $combinedJsonPath = Join-Path $OutputDir "UAL-Operations-Combined.json"
                $allResults | ConvertTo-Json -Depth 10 | Out-File $combinedJsonPath -Encoding $Encoding
                Write-LogFile -Message "[INFO] Saved combined JSON file: $combinedJsonPath" -Level Standard -Color "Green"
            }
            "SOF-ELK" {
                $combinedSofElkPath = Join-Path $OutputDir "UAL-Operations-Combined.json"
                foreach ($item in $allResults) {
                    $item | ConvertTo-Json -Compress -Depth 10 | Out-File $combinedSofElkPath -Append -Encoding UTF8
                }
                Write-LogFile -Message "[INFO] Saved combined SOF-ELK file: $combinedSofElkPath" -Level Standard -Color "Green"
            }
        }
    }
           
    Write-LogFile -Message "`n=== Quick UAL Collection Summary ===" -Color "Cyan" -Level Standard
    Write-LogFile -Message "Operations Searched: $($Operations.Count)" -Level Standard
    Write-LogFile -Message "Total Records Retrieved: $totalRecords" -Level Standard
    Write-LogFile -Message "Output Directory: $OutputDir" -Level Standard
    Write-LogFile -Message "Files Created: JSON and CSV for each operation + combined files" -Level Standard
    Write-LogFile -Message "=============================================" -Color "Cyan" -Level Standard
}

function Test-TaskWillSkip {
    param(
        [string]$TaskName,
        [array]$UserIds
    )
    
    # List of tasks that are skipped when UserIds are provided
    $tenantWideTasks = @(
        "Get-DirectoryActivityLogs",
        "Get-TransportRules", 
        "Get-ConditionalAccessPolicies",
        "Get-Licenses",
        "Get-LicenseCompatibility", 
        "Get-EntraSecurityDefaults",
        "Get-LicensesByUser",
        "Get-Groups",
        "Get-GroupMembers", 
        "Get-DynamicGroups",
        "Get-SecurityAlerts",
        "Get-PIMAssignments",
        "Get-AllRoleActivity"
    )
    
    return ($UserIds.Count -gt 0 -and $TaskName -in $tenantWideTasks)
}