SHELL/5.1.6.1.ps1

$CheckId = "5.1.6.1"
$Title = "Ensure that collaboration invitations are sent to allowed domains only"
$Level = "L2"
$BenchmarkType = "Automated"
$AuditCommands = @(
    '$LegacyUri = "https://graph.microsoft.com/beta/legacy/policies"',
    '(Invoke-MgGraphRequest -Uri $LegacyUri -Method GET).value | Where-Object { $_.type -eq "B2BManagementPolicy" }',
    '$ModernUri = "https://graph.microsoft.com/beta/policies/b2bManagementPolicy"',
    'Invoke-MgGraphRequest -Uri $ModernUri -Method GET'
)

function Get-NestedValue {
    param(
        [AllowNull()][object]$InputObject,
        [string[]]$Path
    )

    $Current = $InputObject
    foreach ($Segment in $Path) {
        if ($null -eq $Current) { return $null }

        if ($Current -is [System.Collections.IDictionary]) {
            if ($Current.Contains($Segment)) {
                $Current = $Current[$Segment]
            }
            else {
                return $null
            }
        }
        else {
            $Prop = $Current.PSObject.Properties[$Segment]
            if ($null -eq $Prop) { return $null }
            $Current = $Prop.Value
        }
    }

    return $Current
}

try {
    if (-not (Get-Command -Name Invoke-MgGraphRequest -ErrorAction SilentlyContinue)) {
        [pscustomobject]@{
            CheckId = $CheckId
            Title = $Title
            Level = $Level
            BenchmarkType = $BenchmarkType
            Status = "ERROR"
            Pass = $null
            Evidence = [pscustomobject]@{
                AuditCommands = $AuditCommands
                SourceDocument = "CIS_Microsoft_365_Foundations_Benchmark_v6.0.1"
            }
            Error = "Invoke-MgGraphRequest cmdlet is unavailable in the current session."
            Timestamp = Get-Date
        }
        return
    }

    $Uri = $null
    $Policies = @()
    $LegacyUri = "https://graph.microsoft.com/beta/legacy/policies"
    $ModernUri = "https://graph.microsoft.com/beta/policies/b2bManagementPolicy"

    try {
        $LegacyResponse = Invoke-MgGraphRequest -Uri $LegacyUri -Method GET -ErrorAction Stop
        $Policies = @($LegacyResponse.value | Where-Object { [string]$_.type -eq 'B2BManagementPolicy' })
        if ($Policies.Count -gt 0) {
            $Uri = $LegacyUri
        }
    }
    catch {
        $Policies = @()
    }

    if ($Policies.Count -eq 0) {
        try {
            $ModernResponse = Invoke-MgGraphRequest -Uri $ModernUri -Method GET -ErrorAction Stop
            if ($null -ne $ModernResponse) {
                $Policies = @([pscustomobject]@{
                        type = "B2BManagementPolicy"
                        definition = $ModernResponse
                    })
                $Uri = $ModernUri
            }
        }
        catch {
            $Policies = @()
        }
    }

    if (@($Policies).Count -eq 0) {
        [pscustomobject]@{
            CheckId = $CheckId
            Title = $Title
            Level = $Level
            BenchmarkType = $BenchmarkType
            Status = "FAIL"
            Pass = $false
            Evidence = [pscustomobject]@{
                AuditCommands = $AuditCommands
                LegacyUri = $LegacyUri
                ModernUri = $ModernUri
                SourceDocument = "CIS_Microsoft_365_Foundations_Benchmark_v6.0.1"
            }
            Error = "No B2B management policy data was returned by either legacy or modern Graph endpoints."
            Timestamp = Get-Date
        }
        return
    }

    $Policy = $Policies | Select-Object -First 1

    $DefinitionRaw = $Policy.definition
    $DefinitionParsed = $null
    if ($DefinitionRaw -is [string]) {
        $DefinitionParsed = $DefinitionRaw | ConvertFrom-Json -Depth 20
    }
    else {
        $DefinitionParsed = $DefinitionRaw
    }

    $DomainsPolicy = Get-NestedValue -InputObject $DefinitionParsed -Path @('B2BManagementPolicy', 'InvitationsAllowedAndBlockedDomainsPolicy')
    if ($null -eq $DomainsPolicy) {
        $DomainsPolicy = Get-NestedValue -InputObject $DefinitionParsed -Path @('InvitationsAllowedAndBlockedDomainsPolicy')
    }

    if ($null -eq $DomainsPolicy) {
        [pscustomobject]@{
            CheckId = $CheckId
            Title = $Title
            Level = $Level
            BenchmarkType = $BenchmarkType
            Status = "FAIL"
            Pass = $false
            Evidence = [pscustomobject]@{
                AuditCommands = $AuditCommands
                Uri = $Uri
                RawDefinition = $DefinitionRaw
                SourceDocument = "CIS_Microsoft_365_Foundations_Benchmark_v6.0.1"
            }
            Error = "InvitationsAllowedAndBlockedDomainsPolicy was not found in returned B2B management policy data."
            Timestamp = Get-Date
        }
        return
    }

    $AllowedDomains = Get-NestedValue -InputObject $DomainsPolicy -Path @('AllowedDomains')
    $BlockedDomainsPresent = $false
    if ($DomainsPolicy -is [System.Collections.IDictionary]) {
        $BlockedDomainsPresent = $DomainsPolicy.Contains('BlockedDomains')
    }
    elseif ($null -ne $DomainsPolicy.PSObject.Properties['BlockedDomains']) {
        $BlockedDomainsPresent = $true
    }

    $AllowedDomainsList = @($AllowedDomains | ForEach-Object { ([string]$_).Trim() } | Where-Object {
        -not [string]::IsNullOrWhiteSpace($_)
    })

    if ($BlockedDomainsPresent) {
        [pscustomobject]@{
            CheckId = $CheckId
            Title = $Title
            Level = $Level
            BenchmarkType = $BenchmarkType
            Status = "FAIL"
            Pass = $false
            Evidence = [pscustomobject]@{
                AuditCommands = $AuditCommands
                Uri = $Uri
                AllowedDomains = @($AllowedDomainsList)
                BlockedDomainsPresent = $true
                DomainsPolicy = $DomainsPolicy
                SourceDocument = "CIS_Microsoft_365_Foundations_Benchmark_v6.0.1"
            }
            Error = "BlockedDomains is present in InvitationsAllowedAndBlockedDomainsPolicy (non-compliant per CIS)."
            Timestamp = Get-Date
        }
        return
    }

    if ($null -eq $AllowedDomains) {
        [pscustomobject]@{
            CheckId = $CheckId
            Title = $Title
            Level = $Level
            BenchmarkType = $BenchmarkType
            Status = "FAIL"
            Pass = $false
            Evidence = [pscustomobject]@{
                AuditCommands = $AuditCommands
                Uri = $Uri
                DomainsPolicy = $DomainsPolicy
                SourceDocument = "CIS_Microsoft_365_Foundations_Benchmark_v6.0.1"
            }
            Error = "AllowedDomains property was not found."
            Timestamp = Get-Date
        }
        return
    }

    $ApprovedDomains = @([string]$env:ROOT365_APPROVED_B2B_DOMAINS -split '[,; ]+' | ForEach-Object {
            $_.Trim().ToLowerInvariant()
        } | Where-Object { $_ })

    if (@($AllowedDomainsList).Count -eq 0) {
        [pscustomobject]@{
            CheckId = $CheckId
            Title = $Title
            Level = $Level
            BenchmarkType = $BenchmarkType
            Status = "PASS"
            Pass = $true
            Evidence = [pscustomobject]@{
                AuditCommands = $AuditCommands
                Uri = $Uri
                AllowedDomains = @()
                BlockedDomainsPresent = $false
                ApprovedDomains = @($ApprovedDomains)
                ApprovedDomainsSource = "Environment variable ROOT365_APPROVED_B2B_DOMAINS"
                SourceDocument = "CIS_Microsoft_365_Foundations_Benchmark_v6.0.1"
            }
            Error = $null
            Timestamp = Get-Date
        }
        return
    }

    $AllowedDomainsLower = @($AllowedDomainsList | ForEach-Object { $_.ToLowerInvariant() })
    $NotApproved = @($AllowedDomainsLower | Where-Object { $_ -notin $ApprovedDomains } | Select-Object -Unique)
    $Pass = ($ApprovedDomains.Count -gt 0 -and $NotApproved.Count -eq 0)

    [pscustomobject]@{
        CheckId = $CheckId
        Title = $Title
        Level = $Level
        BenchmarkType = $BenchmarkType
        Status = if ($Pass) { "PASS" } else { "FAIL" }
        Pass = $Pass
        Evidence = [pscustomobject]@{
            AuditCommands = $AuditCommands
            Uri = $Uri
            AllowedDomains = @($AllowedDomainsList)
            AllowedDomainsLower = @($AllowedDomainsLower)
            ApprovedDomains = @($ApprovedDomains)
            NotApprovedDomains = @($NotApproved)
            ApprovedDomainsSource = "Environment variable ROOT365_APPROVED_B2B_DOMAINS"
            BlockedDomainsPresent = $false
            SourceDocument = "CIS_Microsoft_365_Foundations_Benchmark_v6.0.1"
        }
        Error = if ($Pass) {
            $null
        }
        elseif ($ApprovedDomains.Count -eq 0) {
            "AllowedDomains are configured but ROOT365_APPROVED_B2B_DOMAINS is empty. Define approved domains to complete deterministic validation."
        }
        else {
            "AllowedDomains contains non-approved entries: $($NotApproved -join ', ')."
        }
        Timestamp = Get-Date
    }
}
catch {
    [pscustomobject]@{
        CheckId = $CheckId
        Title = $Title
        Level = $Level
        BenchmarkType = $BenchmarkType
        Status = "ERROR"
        Pass = $null
        Evidence = [pscustomobject]@{
            AuditCommands = $AuditCommands
            SourceDocument = "CIS_Microsoft_365_Foundations_Benchmark_v6.0.1"
        }
        Error = $_.Exception.Message
        Timestamp = Get-Date
    }
}