Checks/SharePoint/ARG.SPO.013.ps1

Register-ArgusCheck @{
    Id          = 'ARG.SPO.013'
    Title       = 'OneDrives near their storage quota (more than {QuotaPercent}%)'
    Area        = 'SharePoint'
    Category    = 'Storage'
    Severity    = 'Low'
    Description = 'OneDrive accounts from the OneDrive usage report (`getOneDriveUsageAccountDetail`, period D180) whose `Storage Used (Byte)` is more than **{QuotaPercent}%** of `Storage Allocated (Byte)`. Deleted accounts and accounts without an allocation are ignored. With concealed report names, or when the report returns no rows, the result is Investigate. When the report leaves `Site URL` empty (a known issue of the usage reports; this report has no Site Id), the URL comes from the `mySite` property of the user, or is derived from the UPN with the OneDrive naming convention when the user no longer exists (`UrlSource` column).'
    Rationale   = 'A full OneDrive stops syncing and saving, and users start keeping files on local disks or personal storage. Accounts close to the limit need a cleanup, a move of team content to SharePoint, or a higher quota.'
    Remediation = @{
        Steps      = "1. Ask the users to move team content to the relevant SharePoint sites and remove duplicates.`n2. Raise the OneDrive quota for users who need it (SharePoint admin center > Settings > OneDrive storage limit, or per user).`n3. Review the default OneDrive storage limit for new users."
        PortalUrl  = 'https://admin.microsoft.com/sharepoint?page=settings&modern=true'
    }
    References  = @(
        @{ Title = 'Set the default storage space for OneDrive users'; Url = 'https://learn.microsoft.com/en-us/sharepoint/set-default-storage-space' }
        @{ Title = 'Microsoft 365 reports in the admin center - OneDrive usage'; Url = 'https://learn.microsoft.com/en-us/microsoft-365/admin/activity-reports/onedrive-for-business-usage-ww' }
    )
    Requires    = @{
        Services    = @('Graph')
        GraphScopes = @('Reports.Read.All', 'User.Read.All', 'Organization.Read.All')
        DataSets    = @('OneDriveUsage', 'ReportSettings', 'Users', 'Organization')
    }
    Parameters  = @{ QuotaPercent = 90 }
    Tags        = @('OneDrive', 'Storage', 'Reports')
    Test        = {
        param($Context)
        $rows = @($Context.Data['OneDriveUsage'] | Where-Object { $_ -and (Get-ArgusProperty $_ 'Is Deleted' '') -ne 'True' })
        $issue = Get-ArgusReportIssue -ReportSettings $Context.Data['ReportSettings'] -Row $rows -ObjectName 'OneDrives near their quota'
        if ($issue) { return New-ArgusResult -Status Investigate -Summary $issue }

        $limit = [double]$Context.Parameters.QuotaPercent
        $evaluated = @($rows | Where-Object { [double](Get-ArgusProperty $_ 'Storage Allocated (Byte)' 0) -gt 0 })
        $findings = @($evaluated | ForEach-Object {
                $used = [double](Get-ArgusProperty $_ 'Storage Used (Byte)' 0)
                $allocated = [double](Get-ArgusProperty $_ 'Storage Allocated (Byte)' 0)
                $percent = [math]::Round(100 * $used / $allocated, 1)
                if ($percent -le $limit) { return }
                [pscustomobject]@{
                    DisplayName       = Get-ArgusProperty $_ 'Owner Display Name' ''
                    UserPrincipalName = Get-ArgusProperty $_ 'Owner Principal Name' ''
                    UsedGB            = [math]::Round($used / 1GB, 2)
                    AllocatedGB       = [math]::Round($allocated / 1GB, 2)
                    UsedPercent       = $percent
                    SiteUrl           = Get-ArgusProperty $_ 'Site URL' ''
                    UrlSource         = 'Report'
                }
            } | Sort-Object UsedPercent -Descending)
        # The report can return an empty Site URL (advisory SP676147) and has no Site Id: resolve it per user.
        $missing = @($findings | Where-Object { -not $_.SiteUrl })
        if ($missing.Count -gt 0) {
            $urls = Resolve-ArgusOneDriveUrl -UserPrincipalName @($missing.UserPrincipalName) -User @($Context.Data['Users']) -Organization $Context.Data['Organization']
            foreach ($finding in $missing) {
                $resolved = $urls[([string]$finding.UserPrincipalName).ToLowerInvariant()]
                if ($resolved) {
                    $finding.SiteUrl = $resolved.Url
                    $finding.UrlSource = if ($resolved.Derived) { 'Derived from the UPN' } else { 'User mySite' }
                }
                else { $finding.UrlSource = 'Not available' }
            }
        }
        $status = if ($findings.Count -gt 0) { 'Failed' } else { 'Passed' }
        New-ArgusResult -Status $status -Findings $findings -EvaluatedCount $evaluated.Count `
            -Summary '{AffectedCount} of {EvaluatedCount} OneDrive accounts use more than {QuotaPercent}% of their storage quota.'
    }
}