Scripts/Start-MESTriage.ps1
function Start-MESTriage { <# .SYNOPSIS Performs a collection triage for specific users or all users—across Azure, Entra ID, and Microsoft 365 environments, based on a customizable template. .DESCRIPTION Executes triage tasks based on template files in the Templates directory. Automatically discovers all available templates (built-in and custom). Users can easily customize templates by commenting/uncommenting tasks. Default templates: - Quick: fastest, essential data - Default: balanced approach - Full: (almost) )everything available .PARAMETER Template Template to use. Available templates are automatically discovered from Templates folder. Built-in: Quick, Standard, Comprehensive Custom: Any .psd1 file in Templates folder .PARAMETER TriageName TriageName is the mandatory parameter specifying the name of the triage project. This will be used as the folder name for outputs. .PARAMETER UserIds UserIds is the parameter specifying the target users for the triage. You can enter multiple email addresses separated by commas. .PARAMETER StartDate StartDate is the parameter specifying the start date of the date range for time-based queries. Default: Today -180 days .PARAMETER EndDate EndDate is the parameter specifying the end date of the date range for time-based queries. Default: Now .PARAMETER Output Output is the parameter specifying the CSV, JSON, JSONL, or SOF-ELK output type. Note: JSONL format is only supported by specific functions (Get-UAL, Get-UALGraph). Other tasks automatically use JSON format regardless of this setting. The Operations from the Unified Audit Log will be collected in CSV and JSON. Default: CSV .PARAMETER MergeOutput MergeOutput is the parameter specifying if you wish to merge outputs to single files where applicable. .PARAMETER OutputDir OutputDir is the parameter specifying the output directory. If not specified, creates Output\[TriageName] Default: Output\[TriageName] .PARAMETER Encoding Encoding is the parameter specifying the encoding of the output files. 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: Minimal .EXAMPLE Start-MESTriage -Template Quick -TriageName "Investigation001" -UserIds "user@domain.com" Performs a quick triage for a single user with project name "Investigation001". .EXAMPLE Start-MESTriage -Template Default -TriageName "Standard" -UserIds "user1@domain.com,user2@domain.com" Performs a standard triage for multiple users. .EXAMPLE Start-MESTriage -Template Comprehensive -TriageName "Comprehensive" -UserIds "user@domain.com" -OutputDir "C:\Investigations" Performs a full comprehensive triage with custom output directory. #> [CmdletBinding()] param ( [string]$Template = "Standard", [string]$StartDate, [string]$EndDate, [Parameter(Mandatory=$true)] [string]$TriageName, [string]$UserIds, [ValidateSet("CSV", "JSON", "JSONL", "SOF-ELK")] [string]$Output = "CSV", [switch]$MergeOutput, [string]$OutputDir, [string]$Encoding = "UTF8", [ValidateSet('None', 'Minimal', 'Standard', 'Debug')] [string]$LogLevel = 'Minimal' ) Set-LogLevel -Level ([LogLevel]::$LogLevel) $isDebugEnabled = $script:LogLevel -eq [LogLevel]::Debug $JSONLSupportedFunctions = @( "Get-UAL", "Get-UALGraph" ) $SOFELKSupportedFunctions = @( "Get-UAL", "Get-UALGraph", "Get-GraphEntraSignInLogs", "Get-GraphEntraAuditLogs" ) $moduleRoot = (Get-Module Microsoft-Extractor-Suite).ModuleBase $templatesDir = Join-Path $moduleRoot "Templates" # Get all available templates $availableTemplates = @() if (Test-Path $templatesDir) { $templateFiles = Get-ChildItem $templatesDir -Filter "*.psd1" $availableTemplates = $templateFiles | ForEach-Object { [System.IO.Path]::GetFileNameWithoutExtension($_.Name) } } # Validate the template parameter if ($Template -notin $availableTemplates) { Write-LogFile -Message "[ERROR] Template '$Template' not found." -Color "Red" -Level Minimal Write-LogFile -Message "[INFO] Available templates: $($availableTemplates -join ', ')" -Level Minimal Write-LogFile -Message "[INFO] Create custom templates by adding .psd1 files to: $templatesDir" -Level Minimal return } # Build template path $templatePath = Join-Path $templatesDir "$Template.psd1" if (!(Test-Path $templatePath)) { Write-LogFile -Message "[ERROR] Template file not found: $templatePath" -Color "Red" -Level Minimal return } try { $templateConfig = Import-PowerShellDataFile -Path $templatePath } catch { Write-LogFile -Message "[ERROR] Failed to load template: $($_.Exception.Message)" -Color "Red" -Level Minimal return } $triageSummary = @{ StartTime = Get-Date ProcessingTime = $null SuccessfulTasks = 0 FailedTasks = 0 SkippedTasks = 0 TotalTasks = 0 TriageType = "$Template Template" TargetUsers = if ($UserIds) { $UserIds } else { "All users" } TemplateName = $Template } $UserIdsArray = if ($UserIds -and $UserIds.ToString().Trim()) { $userString = if ($UserIds -is [array]) { $UserIds -join ',' } else { $UserIds.ToString() } @($userString -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -and $_.Length -gt 0 }) } else { @() } Write-LogFile -Message "=== Starting Quick Triage ===" -Color "Cyan" -Level Minimal Write-LogFile -Message "Project: $TriageName" -Level Minimal Write-LogFile -Message "Template: $Template" -Level Minimal if ($UserIdsArray.Count -eq 0) { Write-LogFile -Message "Target: All users" -Level Minimal } elseif ($UserIdsArray.Count -eq 1) { Write-LogFile -Message "Target User: $($UserIdsArray)" -Level Minimal } else { Write-LogFile -Message "Target Users:" -Level Minimal foreach ($user in $UserIdsArray) { Write-LogFile -Message " - $user" -Level Minimal } } if ($Output -eq "JSONL") { Write-LogFile -Message "Output Format: $Output (supported functions only, others will use JSON)" -Level Minimal } elseif ($Output -eq "SOF-ELK") { Write-LogFile -Message "Output Format: $Output (supported functions only, others will use JSON)" -Level Minimal } else { Write-LogFile -Message "Output Format: $Output" -Level Minimal } Write-LogFile -Message "Start Time: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -Level Minimal if ([string]::IsNullOrEmpty($OutputDir)) { $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" $OutputDir = "Output\$TriageName" } else { $OutputDir = Join-Path $OutputDir $TriageName } if (!(Test-Path $OutputDir)) { New-Item -ItemType Directory -Force -Path $OutputDir > $null Write-LogFile -Message "[INFO] Creating output directory: $OutputDir" -Level Standard } # Validate template has tasks if (!$templateConfig.Tasks -or $templateConfig.Tasks.Count -eq 0) { Write-LogFile -Message "[ERROR] No tasks defined in template" -Color "Red" -Level Minimal return } Write-LogFile -Message "`n==== Executing Template Tasks ====" -Color "Yellow" -Level Minimal Write-LogFile -Message "[INFO] Total tasks to execute: $($templateConfig.Tasks.Count)`n" -Level Minimal foreach ($task in $templateConfig.Tasks) { $triageSummary.TotalTasks++ if ($task -is [string]) { $willSkip = Test-TaskWillSkip -TaskName $task -UserIds $UserIdsArray if (-not $willSkip) { Write-TaskProgress -TaskName $task -Status 'InProgress' } try { $executed = Invoke-TriageTask -TaskName $task -OutputDir $OutputDir -LogLevel $LogLevel -UserIds $UserIdsArray -Output $Output -MergeOutput:$MergeOutput -StartDate $StartDate -EndDate $EndDate -Encoding $Encoding -JSONLSupportedFunctions $JSONLSupportedFunctions -SOFELKSupportedFunctions $SOFELKSupportedFunctions if ($executed -eq $true) { $triageSummary.SuccessfulTasks++ Write-TaskProgress -TaskName $task -Status 'Complete' } elseif ($executed -eq $false) { $triageSummary.FailedTasks++ Write-TaskProgress -TaskName $task -Status 'Failed' -ErrorMessage "Task failed" } elseif ($executed -eq 'SKIP') { $triageSummary.SkippedTasks++ } else { $triageSummary.FailedTasks++ Write-TaskProgress -TaskName $task -Status 'Failed' -ErrorMessage "Task returned unexpected value" } } catch { $triageSummary.FailedTasks++ Write-TaskProgress -TaskName $task -Status 'Failed' -ErrorMessage $_.Exception.Message Write-LogFile -Message "[ERROR] Task $task failed: $($_.Exception.Message)" -Color "Red" -Level Minimal } } elseif ($task -is [hashtable] -and $task.Task -eq "UALOperations") { if ($task.Operations.Count -eq 0) { $triageSummary.SkippedTasks++ continue } else { Write-TaskProgress -TaskName "UAL Operations" -Status 'InProgress' try { $userIdsString = if ($UserIdsArray.Count -gt 0) { $UserIdsArray -join ',' } else { $null } $ualOutput = $Output if ($Output -eq "JSONL" -and "Get-UAL" -in $JSONLSupportedFunctions) { $ualOutput = "JSONL" } elseif ($Output -eq "SOF-ELK" -and "Get-UAL" -in $SOFELKSupportedFunctions) { $ualOutput = "SOF-ELK" } Get-QuickUALOperations -Operations $task.Operations -UserIds $userIdsString -OutputDir $OutputDir -LogLevel $LogLevel -Output $ualOutput -StartDate $StartDate -EndDate $EndDate $triageSummary.SuccessfulTasks++ Write-TaskProgress -TaskName "UAL Operations" -Status 'Complete' } catch { $triageSummary.FailedTasks++ Write-TaskProgress -TaskName "UAL Operations" -Status 'Failed' -ErrorMessage $_.Exception.Message Write-LogFile -Message "[ERROR] UAL Operations failed: $($_.Exception.Message)" -Color "Red" -Level Minimal } } } } $triageSummary.ProcessingTime = (Get-Date) - $triageSummary.StartTime Write-LogFile -Message "`n=== $Template Triage Summary ===" -Color "Cyan" -Level Minimal Write-LogFile -Message "Start Time: $($triageSummary.StartTime.ToString('yyyy-MM-dd HH:mm:ss'))" -Level Minimal Write-LogFile -Message "End Time: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -Level Minimal Write-LogFile -Message "Duration: $($triageSummary.ProcessingTime.ToString('hh\:mm\:ss'))" -Level Minimal Write-LogFile -Message "`nTask Results:" -Level Minimal Write-LogFile -Message " Successful: $($triageSummary.SuccessfulTasks)" -Level Minimal -Color "Green" Write-LogFile -Message " Failed: $($triageSummary.FailedTasks)" -Level Minimal -Color $(if ($triageSummary.FailedTasks -gt 0) { "Red" } else { "Green" }) Write-LogFile -Message " Skipped: $($triageSummary.SkippedTasks)" -Level Minimal -Color "Yellow" Write-LogFile -Message " Total: $($triageSummary.TotalTasks)" -Level Minimal Write-LogFile -Message "`nOutput Location: $OutputDir" -Level Minimal Write-LogFile -Message "=============================================" -Color "Cyan" -Level Minimal } function Invoke-TriageTask { param( [string]$TaskName, [string]$OutputDir, [string]$LogLevel, [array]$UserIds, [string]$Output, [bool]$MergeOutput, [string]$StartDate, [string]$EndDate, [string]$Encoding, [array]$JSONLSupportedFunctions, [array]$SOFELKSupportedFunctions ) function Get-TaskOutputFormat { param( [string]$TaskName, [string]$RequestedOutput, [array]$JSONLSupportedFunctions, [array]$SOFELKSupportedFunctions ) if ($RequestedOutput -eq "JSONL" -and $TaskName -in $JSONLSupportedFunctions) { return "JSONL" } elseif ($RequestedOutput -eq "SOF-ELK" -and $TaskName -in $SOFELKSupportedFunctions) { return "SOF-ELK" } elseif ($RequestedOutput -eq "JSONL" -or $RequestedOutput -eq "SOF-ELK") { # If JSONL or SOF-ELK was requested but not supported, fall back to JSON return "JSON" } else { return $RequestedOutput } } switch ($TaskName) { "Get-RiskyUsers" { if ($UserIds.Count -gt 0) { Get-RiskyUsers -OutputDir $OutputDir -LogLevel $LogLevel -UserIds $UserIds -Encoding $Encoding } else { Get-RiskyUsers -OutputDir $OutputDir -LogLevel $LogLevel -Encoding $Encoding } return $true } "Get-RiskyDetections" { if ($UserIds.Count -gt 0) { Get-RiskyDetections -OutputDir $OutputDir -LogLevel $LogLevel -UserIds $UserIds -Encoding $Encoding } else { Get-RiskyDetections -OutputDir $OutputDir -LogLevel $LogLevel -Encoding $Encoding } return $true } "Get-MFA" { if ($UserIds.Count -gt 0) { Get-MFA -OutputDir $OutputDir -LogLevel $LogLevel -UserIds $UserIds -Encoding $Encoding } else { Get-MFA -OutputDir $OutputDir -LogLevel $LogLevel -Encoding $Encoding } return $true } "Get-MailboxRules" { if ($UserIds.Count -gt 0) { Get-MailboxRules -OutputDir $OutputDir -LogLevel $LogLevel -UserIds $UserIds -Encoding $Encoding } else { Get-MailboxRules -OutputDir $OutputDir -LogLevel $LogLevel -Encoding $Encoding } return $true } "Get-OAuthPermissionsGraph" { if ($UserIds.Count -gt 0) { Get-EntraApplicationsForSpecificUsers -OutputDir $OutputDir -LogLevel $LogLevel -UserIds $UserIds -Encoding $Encoding } else { Get-OAuthPermissionsGraph -OutputDir $OutputDir -LogLevel $LogLevel -Encoding $Encoding } return $true } "Get-GraphEntraSignInLogs" { $taskOutput = Get-TaskOutputFormat -TaskName $TaskName -RequestedOutput $Output -JSONLSupportedFunctions $JSONLSupportedFunctions -SOFELKSupportedFunctions $SOFELKSupportedFunctions $finalOutput = if ($taskOutput -eq 'CSV') { 'JSON' } else { $taskOutput } if ($UserIds.Count -gt 0) { Get-GraphEntraSignInLogs -OutputDir $OutputDir -LogLevel $LogLevel -UserIds $UserIds -Output $finalOutput -MergeOutput -Encoding $Encoding -StartDate $StartDate -EndDate $EndDate } else { Get-GraphEntraSignInLogs -OutputDir $OutputDir -LogLevel $LogLevel -Output $finalOutput -MergeOutput -Encoding $Encoding -StartDate $StartDate -EndDate $EndDate } return $true } "Get-GraphEntraAuditLogs" { $taskOutput = Get-TaskOutputFormat -TaskName $TaskName -RequestedOutput $Output -JSONLSupportedFunctions $JSONLSupportedFunctions -SOFELKSupportedFunctions $SOFELKSupportedFunctions $finalOutput = if ($taskOutput -eq 'CSV') { 'JSON' } else { $taskOutput } $OutputDirAudit = "$OutputDir\Audit logs" New-Item -ItemType Directory -Force -Path $OutputDirAudit > $null $useDateFilter = $true if ($StartDate) { $startDateTime = [DateTime]::Parse($StartDate) $daysDifference = ((Get-Date) - $startDateTime).Days if ($daysDifference -gt 30) { Write-LogFile -Message "[WARNING] Audit logs: Start date is beyond 30-day retention, removing date filter to avoid crash. It will still collect the full 30 days." -Color "Yellow" -Level Minimal $useDateFilter = $false } } $params = @{ OutputDir = $OutputDirAudit LogLevel = $LogLevel Output = $finalOutput Encoding = $Encoding } if ($UserIds.Count -gt 0) { $params.UserIds = $UserIds } if ($useDateFilter) { $params.StartDate = $StartDate $params.EndDate = $EndDate } Get-GraphEntraAuditLogs @params -MergeOutput return $true } "Get-UAL" { $OutputDirAudit = "$OutputDir\Unified Audit Logs" $taskOutput = Get-TaskOutputFormat -TaskName $TaskName -RequestedOutput $Output -JSONLSupportedFunctions $JSONLSupportedFunctions -SOFELKSupportedFunctions $SOFELKSupportedFunctions if ($UserIds.Count -gt 0) { $userIdsString = $UserIds -join ',' Get-UAL -OutputDir $OutputDirAudit -LogLevel $LogLevel -UserIds $userIdsString -Output $taskOutput -Encoding $Encoding -StartDate $StartDate -EndDate $EndDate -MergeOutput } else { Get-UAL -OutputDir $OutputDirAudit -LogLevel $LogLevel -Output $taskOutput -Encoding $Encoding -StartDate $StartDate -EndDate $EndDate -MergeOutput } return $true } "Get-UALStatistics" { if ($UserIds.Count -gt 0) { $userIdsString = $UserIds -join ',' Get-UALStatistics -OutputDir $OutputDir -LogLevel $LogLevel -UserIds $userIdsString -StartDate $StartDate -EndDate $EndDate -WarningAction SilentlyContinue } else { Get-UALStatistics -OutputDir $OutputDir -LogLevel $LogLevel -StartDate $StartDate -EndDate $EndDate -WarningAction SilentlyContinue } return $true } "Get-MailboxAuditLog" { if ($UserIds.Count -gt 0) { $userIdsString = $UserIds -join ',' Get-MailboxAuditLog -OutputDir $OutputDir -LogLevel $LogLevel -UserIds $userIdsString -Encoding $Encoding -StartDate $StartDate -EndDate $EndDate } else { Get-MailboxAuditLog -OutputDir $OutputDir -LogLevel $LogLevel -Encoding $Encoding -StartDate $StartDate -EndDate $EndDate } return $true } "Get-MessageTraceLog" { try { if ($UserIds.Count -gt 0) { Get-MessageTraceLog -OutputDir $OutputDir -LogLevel $LogLevel -UserIds $UserIds -Encoding $Encoding -ErrorAction SilentlyContinue -StartDate $StartDate -EndDate $EndDate } else { Get-MessageTraceLog -OutputDir $OutputDir -LogLevel $LogLevel -Encoding $Encoding -ErrorAction SilentlyContinue -StartDate $StartDate -EndDate $EndDate } return $true } catch { return $false } } "Get-ActivityLogs" { if ($UserIds.Count -gt 0) { Get-ActivityLogs -OutputDir $OutputDir -LogLevel $LogLevel -Encoding $Encoding -UserIds $UserIds -StartDate $StartDate -EndDate $EndDate } else { Get-ActivityLogs -OutputDir $OutputDir -LogLevel $LogLevel -Encoding $Encoding -StartDate $StartDate -EndDate $EndDate } return $true } "Get-DirectoryActivityLogs" { if ($UserIds.Count -gt 0) { return 'SKIP' } Get-DirectoryActivityLogs -OutputDir $OutputDir -LogLevel $LogLevel -Encoding $Encoding -StartDate $StartDate -EndDate $EndDate return $true } "Get-Users" { if ($UserIds.Count -gt 0) { $userIdsString = $UserIds -join ',' Get-Users -OutputDir $OutputDir -LogLevel $LogLevel -UserIds $userIdsString -Encoding $Encoding } else { Get-Users -OutputDir $OutputDir -LogLevel $LogLevel -Encoding $Encoding } return $true } "Get-AdminUsers" { Get-AdminUsers -OutputDir $OutputDir -LogLevel $LogLevel -Encoding $Encoding return $true } "Get-Devices" { if ($UserIds.Count -gt 0) { $userIdsString = $UserIds -join ',' Get-Devices -OutputDir $OutputDir -LogLevel $LogLevel -UserIds $userIdsString -Encoding $Encoding } else { Get-Devices -OutputDir $OutputDir -LogLevel $LogLevel -Encoding $Encoding } return $true } "Get-MailboxAuditStatus" { if ($UserIds.Count -gt 0) { Get-MailboxAuditStatus -OutputDir $OutputDir -LogLevel $LogLevel -UserIds $UserIds -Encoding $Encoding } else { Get-MailboxAuditStatus -OutputDir $OutputDir -LogLevel $LogLevel -Encoding $Encoding } return $true } "Get-MailboxPermissions" { if ($UserIds.Count -gt 0) { Get-MailboxPermissions -OutputDir $OutputDir -LogLevel $LogLevel -UserIds $UserIds -Encoding $Encoding } else { Get-MailboxPermissions -OutputDir $OutputDir -LogLevel $LogLevel -Encoding $Encoding } return $true } "Get-UALGraph" { $taskOutput = Get-TaskOutputFormat -TaskName $TaskName -RequestedOutput $Output -JSONLSupportedFunctions $JSONLSupportedFunctions -SOFELKSupportedFunctions $SOFELKSupportedFunctions if ($UserIds.Count -gt 0) { Get-UALGraph -OutputDir $OutputDir -LogLevel $LogLevel -UserIds $UserIds -Output $taskOutput -Encoding $Encoding -StartDate $StartDate -EndDate $EndDate } else { Get-UALGraph -OutputDir $OutputDir -LogLevel $LogLevel -Output $taskOutput -Encoding $Encoding -StartDate $StartDate -EndDate $EndDate } return $true } "Get-TransportRules" { if ($UserIds.Count -gt 0) { return 'SKIP' } Get-TransportRules -OutputDir $OutputDir -LogLevel $LogLevel -Encoding $Encoding return $true } "Get-ConditionalAccessPolicies" { if ($UserIds.Count -gt 0) { return 'SKIP' } Get-ConditionalAccessPolicies -OutputDir $OutputDir -LogLevel $LogLevel -Encoding $Encoding return $true } "Get-Licenses" { if ($UserIds.Count -gt 0) { return 'SKIP' } Get-Licenses -OutputDir $OutputDir -LogLevel $LogLevel return $true } "Get-LicenseCompatibility" { if ($UserIds.Count -gt 0) { return 'SKIP' } Get-LicenseCompatibility -OutputDir $OutputDir -LogLevel $LogLevel return $true } "Get-EntraSecurityDefaults" { if ($UserIds.Count -gt 0) { return 'SKIP' } Get-EntraSecurityDefaults -OutputDir $OutputDir -LogLevel $LogLevel return $true } "Get-LicensesByUser" { if ($UserIds.Count -gt 0) { return 'SKIP' } Get-LicensesByUser -OutputDir $OutputDir -LogLevel $LogLevel return $true } "Get-Groups" { if ($UserIds.Count -gt 0) { return 'SKIP' } Get-Groups -OutputDir $OutputDir -LogLevel $LogLevel -Encoding $Encoding return $true } "Get-GroupMembers" { if ($UserIds.Count -gt 0) { return 'SKIP' } Get-GroupMembers -OutputDir $OutputDir -LogLevel $LogLevel -Encoding $Encoding return $true } "Get-DynamicGroups" { if ($UserIds.Count -gt 0) { return 'SKIP' } Get-DynamicGroups -OutputDir $OutputDir -LogLevel $LogLevel -Encoding $Encoding return $true } "Get-SecurityAlerts" { if ($UserIds.Count -gt 0) { return 'SKIP' } Get-SecurityAlerts -OutputDir $OutputDir -LogLevel $LogLevel -Encoding $Encoding return $true } "Get-PIMAssignments" { if ($UserIds.Count -gt 0) { return 'SKIP' } Get-PIMAssignments -OutputDir $OutputDir -LogLevel $LogLevel -Encoding $Encoding return $true } "Get-AllRoleActivity" { if ($UserIds.Count -gt 0) { return 'SKIP' } Get-AllRoleActivity -OutputDir $OutputDir -LogLevel $LogLevel -Encoding $Encoding return $true } default { Write-LogFile -Message "[ERROR] Unknown task: $TaskName" -Color "Red" -Level Minimal return $false } } } |