Public/Reporting.ps1
|
<# .SYNOPSIS Get Microsoft 365 audit records using the Graph API. .DESCRIPTION This function queries the Microsoft 365 audit log using a long-running Graph API query. It allows filtering by date and operation, and returns the results as custom objects. .PARAMETER StartDate The start date for the audit log search. Defaults to 180 days ago. .PARAMETER EndDate The end date for the audit log search. Defaults to today. .PARAMETER Operations An array of operations to search for. Defaults to 'FileModified' and 'FileUploaded'. .PARAMETER NoGridView If specified, the function will return the results as an array of objects instead of displaying them in a grid view. .EXAMPLE Get-O365MgAuditRecord -Operations "FileAccessed", "FileModified" -StartDate (Get-Date).AddDays(-30) .EXAMPLE Get-O365MgAuditRecord -NoGridView | Export-Csv -Path "C:\temp\AuditRecords.csv" -NoTypeInformation .NOTES You must be connected to the Microsoft Graph with the 'AuditLogsQuery.Read.All' scope before running this function. Use Connect-MgGraph -Scopes "AuditLogsQuery.Read.All" to connect. #> function Get-O365MgAuditRecord { [CmdletBinding()] param( [datetime]$StartDate = (Get-Date).AddDays(-180), [datetime]$EndDate = (Get-Date), [string[]]$Operations = @("FileModified", "FileUploaded"), [switch]$NoGridView ) Write-Verbose "Starting audit log query." $null = Set-MgRequestContext -MaxRetry 10 -RetryDelay 15 $AuditQueryName = ("Audit Job created at {0}" -f (Get-Date)) $AuditQueryStart = (Get-Date $StartDate -format s) $AuditQueryEnd = (Get-Date $EndDate -format s) $AuditQueryParameters = @{ "displayName" = $AuditQueryName "OperationFilters" = $Operations "filterStartDateTime" = $AuditQueryStart "filterEndDateTime" = $AuditQueryEnd } # Submit the audit query Write-Verbose "Submitting audit query: $AuditQueryName" $AuditJob = New-MgBetaSecurityAuditLogQuery -BodyParameter $AuditQueryParameters # Check the audit query status every 20 seconds until it completes [int]$i = 1 [int]$SleepSeconds = 20 $SearchFinished = $false; [int]$SecondsElapsed = 20 Start-Sleep -Seconds 30 $AuditQueryStatus = Get-MgBetaSecurityAuditLogQuery -AuditLogQueryId $AuditJob.Id while ($SearchFinished -eq $false) { $i++ Write-Verbose ("Waiting for audit search to complete. Check {0} after {1} seconds. Current state {2}" -f $i, $SecondsElapsed, $AuditQueryStatus.status) if ($AuditQueryStatus.status -eq 'succeeded') { $SearchFinished = $true } else { Start-Sleep -Seconds $SleepSeconds $SecondsElapsed = $SecondsElapsed + $SleepSeconds $AuditQueryStatus = Get-MgBetaSecurityAuditLogQuery -AuditLogQueryId $AuditJob.Id } } # Fetch the audit records returned by the query Write-Verbose "Fetching audit records for query: $AuditQueryName" [array]$AuditRecords = Get-MgBetaSecurityAuditLogQueryRecord -AuditLogQueryId $AuditJob.Id -All -PageSize 999 $Report = [System.Collections.Generic.List[Object]]::new() foreach ($Record in $AuditRecords) { $ReportLine = [PSCustomObject]@{ Service = $Record.Service Timestamp = $Record.CreatedDateTime UPN = $Record.userPrincipalName Operation = $record.operation } $Report.Add($ReportLine) } $SortedReport = [array]$Report | Sort-Object { $_.Timestamp -as [datetime] } if ($NoGridView) { $SortedReport } else { $SortedReport | Out-GridView -Title ("Audit Records fetched by query {0}" -f $AuditQueryName) } } <# .SYNOPSIS Get Copilot interaction data from a user's mailbox. .DESCRIPTION This function fetches Copilot interaction data from the TeamsMessagesData folder in a user's mailbox. It can be used to analyze how a user is interacting with Copilot. .PARAMETER UserId The UPN or Id of the user to search. Defaults to the current user. .PARAMETER StartDate The start date for the search. Defaults to 365 days ago. .PARAMETER EndDate The end date for the search. Defaults to today. .PARAMETER NoGridView If specified, the function will return the results as an array of objects instead of displaying them in a grid view. .PARAMETER ShowSummary If specified, a summary of interactions by Copilot app will be displayed in a grid view. .EXAMPLE Get-O365CopilotInteraction -UserId 'alan.dignard@office365itpros.com' .EXAMPLE Get-O365CopilotInteraction -NoGridView | Export-Csv -Path "C:\temp\CopilotInteractions.csv" -NoTypeInformation .NOTES You must be connected to Exchange Online and the Microsoft Graph with the 'User.Read' and 'Mail.Read' scopes before running this function. #> function Get-O365CopilotInteraction { [CmdletBinding()] param( [string]$UserId = (Get-MgContext).Account, [datetime]$StartDate = (Get-Date).AddDays(-365), [datetime]$EndDate = (Get-Date), [switch]$NoGridView, [switch]$ShowSummary ) $User = Get-MgUser -UserId $UserId Write-Verbose "Fetching Copilot interactions for user: $($User.DisplayName)" [array]$Folders = Get-ExoMailboxFolderStatistics -Identity $User.Id -FolderScope NonIPMRoot | Select-Object Name, FolderId $TeamsMessagesData = $Folders | Where-Object {$_.Name -eq "TeamsMessagesData"} If ($TeamsMessagesData) { $FolderId = $TeamsMessagesData.FolderId } Else { Write-Warning "TeamsMessagesData folder not found for user: $($User.DisplayName)" return } $RestId = Convert-StoreIdToRestId -StoreId $FolderId -UserId $User.Id Write-Verbose ("The RestId for the TeamsMessagesData folder is {0}" -f $RestId) $CP0 = "Microsoft 365 Chat" $CP1 = "Copilot in Word" $CP2 = "Copilot in Outlook" $CP3 = "Copilot in PowerPoint" $CP4 = "Copilot in Excel" $CP5 = "Copilot in Teams" $CP6 = "Copilot in Stream" $CP7 = "Copilot in OneNote" $CP8 = "Copilot in Loop" $CP9 = "Copilot in SharePoint" $CP10 = "Microsoft Copilot" [string]$StartDateStr = Get-Date $StartDate -Format "yyyy-MM-ddTHH:mm:ssZ" [string]$EndDateStr = Get-Date $EndDate -Format "yyyy-MM-ddTHH:mm:ssZ" $null = Set-MgRequestContext -MaxRetry 10 -RetryDelay 15 Write-Verbose "Fetching messages sent by Copilot from the TeamsMessagesData folder" # Find messages sent by Copilot [array]$Items = Get-MgUserMailFolderMessage -UserId $User.Id -MailFolderId 'TeamsMessagesData' -All -PageSize 500 ` -Filter "(ReceivedDateTime ge $StartDateStr and ReceivedDateTime le $EndDateStr) ` and (sender/emailAddress/name eq '$CP0' or sender/emailAddress/name eq '$CP1' or sender/emailAddress/name eq '$CP2' ` or sender/emailAddress/name eq '$CP3' or sender/emailAddress/name eq '$CP4' or sender/emailAddress/name eq '$CP5' ` or sender/emailAddress/name eq '$CP6' or sender/emailAddress/name eq '$CP7' or sender/emailAddress/name eq '$CP8' or sender/emailAddress/name eq '$CP9' or sender/emailAddress/name eq '$CP10')" -Property Sender, SentDateTime, BodyPreview, ToRecipients Write-Verbose "Finding messages received by Copilot..." # Now try and find messages received by Copilot [array]$ItemsReceived = Get-MgUserMailFolderMessage -UserId $User.Id -MailFolderId 'TeamsMessagesData' ` -All -PageSize 500 -Property Sender, SentDateTime, BodyPreview, ToRecipients ` -Filter "(ReceivedDateTime ge $StartDateStr and ReceivedDateTime le $EndDateStr) ` AND (singleValueExtendedProperties/any(ep:ep/id eq 'String 0x0E04' and contains(ep/value,'Copilot in')))" # And because we have some prompts received by "Microsoft 365 Chat", we need to find them too [array]$ItemsChat = Get-MgUserMailFolderMessage -UserId $User.Id -MailFolderId 'TeamsMessagesData' ` -All -PageSize 500 -Property Sender, SentDateTime, BodyPreview, ToRecipients ` -Filter "(ReceivedDateTime ge $StartDateStr and ReceivedDateTime le $EndDateStr) ` AND (singleValueExtendedProperties/any(ep:ep/id eq 'String 0x0E04' and ep/value eq 'Microsoft 365 Chat'))" if ($ItemsReceived) { $Items = $Items + $ItemsReceived } if ($ItemsChat) { $Items = $Items + $ItemsChat } Write-Verbose ("Found {0} messages sent and received by Copilot in the TeamsMessagesData folder" -f $Items.Count) $Report = [System.Collections.Generic.List[Object]]::new() ForEach ($Item in $Items) { $ReportLine = [PSCustomObject][Ordered]@{ Sender = $Item.Sender.emailaddress.Name To = $Item.Torecipients.emailaddress.name -join "," Sent = $Item.SentDateTime Body = $Item.BodyPreview } $Report.Add($ReportLine) } $SortedReport = [array]$Report | Sort-Object { $_.Sent -as [datetime] } if ($NoGridView) { $SortedReport } else { $SortedReport | Out-GridView -Title "Copilot Interactions" } if ($ShowSummary) { $ReportCopilot = $SortedReport | Where-Object {$_.Sender -ne $User.displayName} $ReportCopilot | Group-Object -Property Sender | Select-Object Name, Count | Sort-Object Count -Descending | Out-GridView -Title "Copilot Interactions by App" } } <# .SYNOPSIS Fetches service messages from the Microsoft Graph. .DESCRIPTION This function fetches service messages (Message Center posts) from the Microsoft Graph and provides a report and analysis of the data. .PARAMETER ExportPath If specified, the function will export the results to a CSV file at the given path. .PARAMETER NoGridView If specified, the function will not display the results in a grid view. .PARAMETER ShowAnalysis If specified, a detailed analysis of the messages will be displayed. .EXAMPLE Get-O365ServiceMessage -ExportPath "C:\temp\ServiceMessages.csv" -ShowAnalysis .NOTES You must be connected to the Microsoft Graph with the 'ServiceMessage.Read.All' scope before running this function. #> function Get-O365ServiceMessage { [CmdletBinding()] param( [string]$ExportPath, [switch]$NoGridView, [switch]$ShowAnalysis ) Write-Verbose "Fetching Microsoft 365 Message Center Notifications..." [array]$MCPosts = Get-MgServiceAnnouncementMessage -Sort 'LastmodifiedDateTime desc' -All Write-Verbose "Generating a report..." $Report = [System.Collections.Generic.List[Object]]::new() ForEach ($M in $MCPosts) { [array]$Services = $M.Services if ([string]::IsNullOrEmpty($M.ActionRequiredByDateTime)) { $ActionRequiredDate = $null } else { $ActionRequiredDate = Get-Date($M.ActionRequiredByDateTime) -format "dd-MMM-yyyy" } $Age = New-TimeSpan($M.LastModifiedDateTime) $AgeSinceStart = New-TimeSpan($M.StartDateTime) $Body = $M | Select-Object -ExpandProperty Body $HTML = New-Object -Com "HTMLFile" $HTML.write([ref]$body.content) $MCPostText = $HTML.body.innerText $ReportLine = [PSCustomObject] @{ MessageId = $M.Id Title = $M.Title Workloads = ($Services -join ",") Category = $M.category 'Start Time' = Get-Date($M.StartDateTime) -format "dd-MMM-yyyy HH:mm" 'End Time' = Get-Date($M.EndDateTime) -format "dd-MMM-yyyy HH:mm" 'Last Update' = Get-Date($M.LastModifiedDateTime) -format "dd-MMM-yyyy HH:mm" 'Action Required by' = $ActionRequiredDate MessageText = $MCPostText Age = ("{0} days {1} hours" -f $Age.Days.ToString(), $Age.Hours.ToString()) IsRead = $M.ViewPoint.IsRead IsDismissed = $M.ViewPoint.IsDismissed MinutesSinceUpdate = $Age.TotalMinutes MinutesSinceStart = $AgeSinceStart.TotalMinutes } $Report.Add($ReportLine) } $SortedReport = $Report | Sort-Object {$_.'Last Update' -as [DateTime]} -Descending if (-not $NoGridView) { $SortedReport | Select-Object MessageId, Title, Category, 'Last Update', 'Action Required By', Age | Out-GridView } if ($ExportPath) { try { $SortedReport | Export-CSV -NoTypeInformation $ExportPath Write-Verbose "Report file saved to $ExportPath" } catch { Write-Warning "Failed to save report file to $ExportPath. Error: $_" } } if ($ShowAnalysis) { # Figure out how many MC posts are for each workload $WorkloadCounts = $SortedReport | Group-Object -Property Workloads | Select-Object @{Name="Workload"; Expression={$_.Name}}, Count $DelayedPosts = $SortedReport | Where-Object {$_.Title -like "*(Updated)*"} $DelayedWorkloadCounts = $DelayedPosts | Group-Object -Property Workloads | Select-Object @{Name="Workload"; Expression={$_.Name}}, Count $Analysis = foreach($Workload in $WorkloadCounts) { $DelayedCount = ($DelayedWorkloadCounts | Where-Object {$_.Workload -eq $Workload.Workload}).Count $PercentDelayed = if($Workload.Count -gt 0) {($DelayedCount / $Workload.Count).ToString('P')} else {'N/A'} [PSCustomObject]@{ Workload = $Workload.Workload TotalPosts = $Workload.Count DelayedPosts = $DelayedCount PercentDelayed = $PercentDelayed } } Write-Output $Analysis } return $SortedReport } <# .SYNOPSIS Searches the admin audit log. .DESCRIPTION This function searches the admin audit log for specific operations. .PARAMETER Operations An array of operations to search for. .PARAMETER StartDate The start date for the audit log search. .PARAMETER EndDate The end date for the audit log search. .PARAMETER UserIds An array of user IDs to filter the search by. .EXAMPLE Get-O365AdminAuditLog -Operations 'New-Mailbox' -StartDate (Get-Date).AddDays(-7) .NOTES You must be connected to Exchange Online before running this function. #> function Get-O365AdminAuditLog { [CmdletBinding()] param( [string[]]$Operations, [datetime]$StartDate = (Get-Date).AddDays(-90), [datetime]$EndDate = (Get-Date).AddDays(1), [string[]]$UserIds ) Write-Verbose "Searching the admin audit log..." $AuditLog = Search-UnifiedAuditLog -StartDate $StartDate -EndDate $EndDate -Operations $Operations -UserIds $UserIds -ResultSize 5000 $Report = [System.Collections.Generic.List[Object]]::new() foreach ($Record in $AuditLog) { $AuditData = ConvertFrom-Json $Record.AuditData $ReportLine = [PSCustomObject]@{ TimeStamp = $Record.CreationDate User = $Record.UserIds Operation = $Record.Operations Result = $AuditData.ResultStatus Object = $AuditData.ObjectId Details = $AuditData } $Report.Add($ReportLine) } return $Report } <# .SYNOPSIS Generates a report of mailbox permissions. .DESCRIPTION This function generates a detailed report of all mailbox permissions (FullAccess, SendAs, SendOnBehalf) for all mailboxes in the tenant. .EXAMPLE Get-O365MailboxPermissionsReport .NOTES You must be connected to Exchange Online before running this function. #> function Get-O365MailboxPermissionsReport { [CmdletBinding()] param() Write-Verbose "Generating mailbox permissions report..." $Mailboxes = Get-Mailbox -ResultSize Unlimited $Report = [System.Collections.Generic.List[Object]]::new() foreach ($Mailbox in $Mailboxes) { Write-Verbose "Checking mailbox: $($Mailbox.DisplayName)" # Full Access $FullAccess = Get-MailboxPermission -Identity $Mailbox.Identity | Where-Object { $_.AccessRights -eq 'FullAccess' -and -not $_.IsInherited } foreach ($Permission in $FullAccess) { $ReportLine = [PSCustomObject]@{ Mailbox = $Mailbox.DisplayName User = $Permission.User Permission = 'FullAccess' IsExternal = $Permission.User -like '*#EXT#*' } $Report.Add($ReportLine) } # Send As $SendAs = Get-RecipientPermission -Identity $Mailbox.Identity | Where-Object { $_.Trustee -ne $Mailbox.DisplayName } foreach ($Permission in $SendAs) { $ReportLine = [PSCustomObject]@{ Mailbox = $Mailbox.DisplayName User = $Permission.Trustee Permission = 'SendAs' IsExternal = $Permission.Trustee -like '*#EXT#*' } $Report.Add($ReportLine) } # Send on Behalf if ($Mailbox.GrantSendOnBehalfTo) { foreach ($Trustee in $Mailbox.GrantSendOnBehalfTo) { $ReportLine = [PSCustomObject]@{ Mailbox = $Mailbox.DisplayName User = $Trustee.Name Permission = 'SendOnBehalf' IsExternal = $false # Cannot be external } $Report.Add($ReportLine) } } } return $Report } |