Checks/GroupsTeams/ARG.GRP.006.ps1

Register-ArgusCheck @{
    Id          = 'ARG.GRP.006'
    Title       = 'Teams with guests and no owner'
    Area        = 'GroupsTeams'
    Category    = 'Ownership'
    Severity    = 'High'
    Description = 'Teams (Microsoft 365 groups with a team) that have no owner and at least one guest member. Members of the ownerless Teams are read with JSON batching (`GET /groups/<id>/members/microsoft.graph.user`, first 999 members).'
    Rationale   = 'Guests in a team without owners keep access to its chats, files and site, and nobody reviews or removes them. These Teams are the first to fix among ownerless groups (ARG.GRP.004).'
    Remediation = @{
        Steps      = "1. Assign at least two owners to each team.`n2. Ask the new owners to review the guest members and remove the ones who no longer need access.`n3. Consider access reviews for guests in Teams (Microsoft Entra ID Governance)."
        PortalUrl  = 'https://admin.teams.microsoft.com/teams/manage'
    }
    References  = @(
        @{ Title = 'Guest access in Microsoft Teams'; Url = 'https://learn.microsoft.com/en-us/microsoftteams/guest-access' }
        @{ Title = 'Manage guest access with access reviews'; Url = 'https://learn.microsoft.com/en-us/entra/id-governance/manage-guest-access-with-access-reviews' }
    )
    Requires    = @{
        Services    = @('Graph')
        GraphScopes = @('Group.Read.All', 'GroupMember.Read.All')
        DataSets    = @('Groups')
    }
    Tags        = @('Teams', 'Guests', 'Ownership')
    Test        = {
        param($Context)
        $teams = @($Context.Data['Groups'] | Where-Object { $_.IsTeam })
        $ownerless = @($teams | Where-Object { $_.OwnerCount -eq 0 })
        if ($ownerless.Count -eq 0) {
            return New-ArgusResult -Status Passed -EvaluatedCount $teams.Count -Summary 'All {EvaluatedCount} Teams have at least one owner.'
        }

        $requests = @($ownerless | ForEach-Object {
                $id = [string](Get-ArgusProperty $_ 'id' '')
                @{ Id = $id; Url = "/groups/$id/members/microsoft.graph.user?`$select=id,userType,userPrincipalName&`$top=999" }
            })
        $members = @{}
        $unread = 0
        foreach ($response in @(Invoke-ArgusGraphBatch -Request $requests)) {
            if ($response.Status -eq 200) { $members[$response.Id] = @(Get-ArgusProperty $response.Body 'value' @()) } else { $unread++ }
        }

        $findings = @($ownerless | ForEach-Object {
                $id = [string](Get-ArgusProperty $_ 'id' '')
                if (-not $members.ContainsKey($id)) { return }
                $guests = @($members[$id] | Where-Object { (Get-ArgusProperty $_ 'userType' '') -eq 'Guest' })
                if ($guests.Count -eq 0) { return }
                [pscustomobject]@{
                    DisplayName = Get-ArgusProperty $_ 'displayName' ''
                    Guests      = $guests.Count
                    Members     = $members[$id].Count
                    GuestUsers  = @($guests | ForEach-Object { Get-ArgusProperty $_ 'userPrincipalName' '' })
                    Visibility  = Get-ArgusProperty $_ 'visibility' ''
                    GroupId     = $id
                }
            } | Sort-Object Guests -Descending)

        $note = if ($unread) { " Members of $unread ownerless Teams could not be read." } else { '' }
        if ($findings.Count -eq 0) {
            return New-ArgusResult -Status Passed -EvaluatedCount $teams.Count -Summary ("None of the $($ownerless.Count) ownerless Teams has guest members." + $note)
        }
        New-ArgusResult -Status Failed -Findings $findings -EvaluatedCount $teams.Count `
            -Summary ("{AffectedCount} of {EvaluatedCount} Teams have guest members and no owner." + $note)
    }
}