SHELL/5.3.3.ps1

$CheckId = "5.3.3"
$Title = "Ensure 'Access reviews' for privileged roles are configured"
$Level = "L1"
$BenchmarkType = "Automated"
$Uri = "https://graph.microsoft.com/v1.0/identityGovernance/accessReviews/definitions"

function Get-PropValue {
    param(
        [AllowNull()]$Object,
        [string]$Name
    )

    if ($null -eq $Object) {
        return $null
    }

    if ($Object -is [hashtable]) {
        foreach ($Key in $Object.Keys) {
            if ([string]$Key -ieq $Name) {
                return $Object[$Key]
            }
        }
    }

    if ($Object.PSObject -and $Object.PSObject.Properties) {
        foreach ($Property in $Object.PSObject.Properties) {
            if ([string]$Property.Name -ieq $Name) {
                return $Property.Value
            }
        }
    }

    return $null
}

function Get-PrivilegedRoleScopeQueries {
    param([AllowNull()]$Review)

    $Queries = [System.Collections.Generic.List[string]]::new()
    $Scope = Get-PropValue -Object $Review -Name "scope"
    if ($Scope) {
        $DirectScopeQuery = Get-PropValue -Object $Scope -Name "query"
        if ($DirectScopeQuery) {
            $Queries.Add([string]$DirectScopeQuery)
        }

        $PrincipalScopes = @(Get-PropValue -Object $Scope -Name "principalScopes")
        foreach ($PrincipalScope in $PrincipalScopes) {
            $PrincipalQuery = Get-PropValue -Object $PrincipalScope -Name "query"
            if ($PrincipalQuery) {
                $Queries.Add([string]$PrincipalQuery)
            }
        }
    }

    return @($Queries)
}

try {
    $Response = Invoke-MgGraphRequest -Uri $Uri -Method GET -ErrorAction Stop
    $Definitions = @(Get-PropValue -Object $Response -Name "value")

    if ($Definitions.Count -eq 0) {
        [pscustomobject]@{
            CheckId = $CheckId
            Title = $Title
            Level = $Level
            BenchmarkType = $BenchmarkType
            Status = "FAIL"
            Pass = $false
            Evidence = [pscustomobject]@{
                ReviewCount = 0
                PrivilegedRoleReviewCount = 0
                CompliantPrivilegedRoleReviewCount = 0
                AccessReviewReport = @()
                SourceDocument = "CIS_Microsoft_365_Foundations_Benchmark_v6.0.1"
            }
            Error = "No access review definitions were returned."
            Timestamp = Get-Date
        }
        return
    }

    $PrivilegedRoleReviews = @()
    foreach ($Review in $Definitions) {
        $Queries = Get-PrivilegedRoleScopeQueries -Review $Review
        if (($Queries -join " || ") -match "(?i)roleAssignmentScheduleInstances|roleDefinitions|directoryRoles") {
            $PrivilegedRoleReviews += $Review
        }
    }

    $AccessReviewReport = foreach ($Review in $PrivilegedRoleReviews) {
        $Settings = Get-PropValue -Object $Review -Name "settings"
        $Recurrence = Get-PropValue -Object $Settings -Name "recurrence"
        $Pattern = Get-PropValue -Object $Recurrence -Name "pattern"
        $RecurrenceType = [string](Get-PropValue -Object $Pattern -Name "type")

        $Status = [string](Get-PropValue -Object $Review -Name "status")
        $Reviewers = @(Get-PropValue -Object $Review -Name "reviewers")
        $Mail = [bool](Get-PropValue -Object $Settings -Name "mailNotificationsEnabled")
        $Reminder = [bool](Get-PropValue -Object $Settings -Name "reminderNotificationsEnabled")
        $Justification = [bool](Get-PropValue -Object $Settings -Name "justificationRequiredOnApproval")
        $AutoApply = [bool](Get-PropValue -Object $Settings -Name "autoApplyDecisionsEnabled")
        $DefaultDecision = [string](Get-PropValue -Object $Settings -Name "defaultDecision")
        $DurationInDaysRaw = Get-PropValue -Object $Settings -Name "instanceDurationInDays"
        $DurationInDays = 0
        [void][int]::TryParse([string]$DurationInDaysRaw, [ref]$DurationInDays)

        $RecurrencePass = $RecurrenceType -in @("weekly", "absoluteMonthly", "relativeMonthly")
        $DurationPass = ($DurationInDays -gt 0 -and $DurationInDays -le 14)
        $NoResponseNoChange = ($DefaultDecision -match '^(?i:none)$')
        $ReviewersPass = (@($Reviewers).Count -gt 0)

        $IsCISCompliant =
            ($Status -eq "InProgress") -and
            $ReviewersPass -and
            $Mail -and
            $Reminder -and
            $Justification -and
            $RecurrencePass -and
            $DurationPass -and
            $AutoApply -and
            $NoResponseNoChange

        [pscustomobject]@{
            Name = [string](Get-PropValue -Object $Review -Name "displayName")
            Status = $Status
            ReviewersCount = @($Reviewers).Count
            mailNotificationsEnabled = $Mail
            reminderNotificationsEnabled = $Reminder
            justificationRequiredOnApproval = $Justification
            Frequency = $RecurrenceType
            instanceDurationInDays = $DurationInDays
            autoApplyDecisionsEnabled = $AutoApply
            defaultDecision = $DefaultDecision
            IsCISCompliant = $IsCISCompliant
        }
    }

    $CompliantCount = @($AccessReviewReport | Where-Object { $_.IsCISCompliant }).Count
    $Pass = $CompliantCount -gt 0
    $Status = if ($Pass) { "PASS" } else { "FAIL" }

    if ($PrivilegedRoleReviews.Count -eq 0) {
        $Pass = $false
        $Status = "FAIL"
    }

    [pscustomobject]@{
        CheckId = $CheckId
        Title = $Title
        Level = $Level
        BenchmarkType = $BenchmarkType
        Status = $Status
        Pass = $Pass
        Evidence = [pscustomobject]@{
            ReviewCount = $Definitions.Count
            PrivilegedRoleReviewCount = $PrivilegedRoleReviews.Count
            CompliantPrivilegedRoleReviewCount = $CompliantCount
            AccessReviewReport = @($AccessReviewReport)
            SourceDocument = "CIS_Microsoft_365_Foundations_Benchmark_v6.0.1"
        }
        Error = if ($Pass) { $null } else { "No privileged-role access review matched all CIS-required settings, or no privileged-role access review was found." }
        Timestamp = Get-Date
    }
}
catch {
    [pscustomobject]@{
        CheckId = $CheckId
        Title = $Title
        Level = $Level
        BenchmarkType = $BenchmarkType
        Status = "ERROR"
        Pass = $null
        Evidence = [pscustomobject]@{
            RequestUri = $Uri
            SourceDocument = "CIS_Microsoft_365_Foundations_Benchmark_v6.0.1"
        }
        Error = $_.Exception.Message
        Timestamp = Get-Date
    }
}