Checks/Exchange/ARG.EXO.004.ps1

Register-ArgusCheck @{
    Id          = 'ARG.EXO.004'
    Title       = 'Inactive user mailboxes (more than {InactiveDays} days)'
    Area        = 'Exchange'
    Category    = 'Lifecycle'
    Severity    = 'Low'
    Description = 'Mailboxes whose `Last Activity Date` in the mailbox usage report (`getMailboxUsageDetail`, period D180) is older than **{InactiveDays}** days, or empty for mailboxes created before that. When Exchange Online is connected only user mailboxes are evaluated; otherwise the report also contains shared mailboxes, which is noted in the summary. With concealed report names, or when the report returns no rows, the result is Investigate.'
    Rationale   = 'A user mailbox nobody uses usually belongs to a leaver or a forgotten test account: it keeps a licence, mail and permissions without a reason.'
    Remediation = @{
        Steps      = "1. Confirm with the manager whether the mailbox is still needed.`n2. If the content must be kept, convert it to a shared mailbox or apply a hold, then remove the licence.`n3. Otherwise disable and delete the account after your retention period."
        PortalUrl  = 'https://admin.exchange.microsoft.com/#/mailboxes'
    }
    References  = @(
        @{ Title = 'Microsoft 365 reports in the admin center - Mailbox usage'; Url = 'https://learn.microsoft.com/en-us/microsoft-365/admin/activity-reports/mailbox-usage' }
        @{ Title = 'Convert a user mailbox to a shared mailbox'; Url = 'https://learn.microsoft.com/en-us/microsoft-365/admin/email/convert-user-mailbox-to-shared-mailbox' }
    )
    Requires    = @{
        Services         = @('Graph')
        GraphScopes      = @('Reports.Read.All')
        DataSets         = @('MailboxUsage', 'ReportSettings')
        OptionalDataSets = @('Mailboxes')
    }
    Parameters  = @{ InactiveDays = 90 }
    Tags        = @('Exchange', 'Lifecycle', 'Reports')
    Test        = {
        param($Context)
        $rows = @($Context.Data['MailboxUsage'] | Where-Object { $_ -and (Get-ArgusProperty $_ 'Is Deleted' '') -ne 'True' })
        $issue = Get-ArgusReportIssue -ReportSettings $Context.Data['ReportSettings'] -Row $rows -ObjectName 'inactive mailboxes'
        if ($issue) { return New-ArgusResult -Status Investigate -Summary $issue }

        $note = ''
        if ($Context.Data.ContainsKey('Mailboxes')) {
            $userMailboxes = @{}
            foreach ($mailbox in @($Context.Data['Mailboxes'] | Where-Object { (Get-ArgusProperty $_ 'RecipientTypeDetails' '') -eq 'UserMailbox' })) {
                $upn = [string](Get-ArgusProperty $mailbox 'UserPrincipalName' '')
                if ($upn) { $userMailboxes[$upn.ToLowerInvariant()] = $true }
            }
            $rows = @($rows | Where-Object { $userMailboxes.ContainsKey(([string](Get-ArgusProperty $_ 'User Principal Name' '')).ToLowerInvariant()) })
        }
        else {
            $note = ' Exchange Online is not connected, so shared mailboxes could not be excluded.'
        }

        $threshold = $Context.Now.AddDays(-[int]$Context.Parameters.InactiveDays)
        $evaluated = @($rows | Where-Object { $created = ConvertTo-ArgusUtcDate (Get-ArgusProperty $_ 'Created Date' ''); -not $created -or $created -le $threshold })
        $findings = @($evaluated | ForEach-Object {
                $last = ConvertTo-ArgusUtcDate (Get-ArgusProperty $_ 'Last Activity Date' '')
                if ($last -and $last -ge $threshold) { return }
                [pscustomobject]@{
                    DisplayName       = Get-ArgusProperty $_ 'Display Name' ''
                    UserPrincipalName = Get-ArgusProperty $_ 'User Principal Name' ''
                    LastActivity      = Format-ArgusDate $last 'None in the last 180 days'
                    DaysInactive      = if ($last) { [int]($Context.Now - $last).TotalDays } else { $null }
                    StorageUsedGB     = [math]::Round([double](Get-ArgusProperty $_ 'Storage Used (Byte)' 0) / 1GB, 2)
                    Created           = Format-ArgusDate (Get-ArgusProperty $_ 'Created Date' '')
                }
            } | Sort-Object @{ Expression = { if ($null -eq $_.DaysInactive) { [int]::MaxValue } else { $_.DaysInactive } }; Descending = $true }, UserPrincipalName)
        $status = if ($findings.Count -gt 0) { 'Failed' } else { 'Passed' }
        New-ArgusResult -Status $status -Findings $findings -EvaluatedCount $evaluated.Count `
            -Summary ('{AffectedCount} of {EvaluatedCount} mailboxes have had no activity for {InactiveDays}+ days.' + $note)
    }
}