Private/22a-RoleProtection.ps1

function Add-AtlasRoleProtectionEvidence {
    [CmdletBinding()]
    param([string] $TenantId, [AtlasCollectionResult] $Result)

    $metrics = @{ requestCount = 0; retryCount = 0 }
    $endpoints = @{
        schedules = '/v1.0/roleManagement/directory/roleAssignmentScheduleInstances?$select=id,roleAssignmentOriginId,principalId,roleDefinitionId,directoryScopeId,appScopeId,assignmentType,startDateTime,endDateTime,memberType'
        policies = '/v1.0/policies/roleManagementPolicyAssignments?$filter=scopeId%20eq%20%27/%27%20and%20scopeType%20eq%20%27DirectoryRole%27&$expand=policy($expand=rules)'
    }
    foreach ($kind in @('schedules', 'policies')) {
        try {
            $response = Invoke-AtlasGraphRequest -Uri $endpoints[$kind]
            $metrics.requestCount += $response.Metrics.requestCount
            $metrics.retryCount += $response.Metrics.retryCount
            foreach ($item in $response.Items) {
                if ($kind -eq 'schedules') {
                    $origin = Get-AtlasResponseProperty -InputObject $item -Name roleAssignmentOriginId
                    if (-not $origin) { continue }
                    foreach ($edge in $Result.Edges) {
                        if ($edge.Relationship -ne 'assignedRole' -or $edge.State.assignmentId -ne $origin) { continue }
                        $fields = @{}
                        foreach ($field in @('assignmentType', 'startDateTime', 'endDateTime', 'memberType', 'roleAssignmentOriginId')) {
                            $fields[$field] = Get-AtlasResponseProperty -InputObject $item -Name $field
                        }
                        $evidence = New-AtlasEvidence -TenantId $TenantId -Collector directoryRoleProtection -Endpoint $endpoints[$kind] -SourceObjectId $item.id -Fields $fields
                        $Result.Evidence.Add($evidence)
                        # Preserve all matching instances; overlapping schedules must not overwrite one another.
                        if (-not $edge.State.ContainsKey('scheduleInstances')) { $edge.State.scheduleInstances = @() }
                        $edge.State.scheduleInstances += $fields
                        $edge.EvidenceIds = @($edge.EvidenceIds) + @($evidence.Key)
                    }
                }
                else {
                    $policy = Get-AtlasResponseProperty -InputObject $item -Name policy
                    $rules = @(Get-AtlasResponseProperty -InputObject $policy -Name rules | Where-Object { $null -ne $_ })
                    foreach ($role in $Result.Nodes) {
                        if ($role.Kind -ne 'roleDefinition' -or $role.Id -ne $item.roleDefinitionId) { continue }
                        $fields = @{ policyId = $item.policyId; scopeId = $item.scopeId; scopeType = $item.scopeType; rules = $rules }
                        $evidence = New-AtlasEvidence -TenantId $TenantId -Collector directoryRoleProtection -Endpoint $endpoints[$kind] -SourceObjectId $item.id -Fields $fields
                        if (-not $rules.Count -or (Get-AtlasResponseProperty -InputObject $policy -Name 'rules@odata.nextLink')) {
                            $evidence.Completeness = 'partial'
                            $Result.Status = 'partial'
                            $Result.Warnings.Add('A PIM role policy has missing or incomplete expanded rules. Activation requirements remain unverified.')
                        }
                        $Result.Evidence.Add($evidence)
                        if (-not $role.Properties.ContainsKey('pimPolicies')) { $role.Properties.pimPolicies = @() }
                        $role.Properties.pimPolicies += @{ policyId = $item.policyId; scopeId = $item.scopeId; rules = $rules; evidenceId = $evidence.Key }
                    }
                }
            }
        }
        catch [System.Management.Automation.PipelineStoppedException] { throw }
        catch [System.OperationCanceledException] { throw }
        catch {
            $Result.Status = 'partial'
            $Result.Warnings.Add("PIM protection $kind evidence could not be collected: $(Get-AtlasSafeErrorDetail -ErrorRecord $_)")
        }
    }
    return $metrics
}