Assessment/EffectivePermissions.ps1
|
# Evaluate a complete, ordered ACL path and identity graph against an approved policy. function Get-AzDevOpsEffectivePermissionAssessment { [CmdletBinding()] param([object]$Evidence, [object]$Policy) $unknown = [pscustomobject]@{ Outcome = 'Unknown'; ReasonCode = 'PermissionEvidenceUnavailable'; Violations = @() } if ($null -eq $Policy -or -not $Policy.Reference -or $null -eq $Policy.SensitiveMask -or $null -eq $Policy.Allowed) { return [pscustomobject]@{ Outcome = 'Unknown'; ReasonCode = 'PermissionPolicyUnavailable'; Violations = @() } } if ($null -eq $Evidence -or $Evidence.SchemaVersion -ne 1 -or $Evidence.Status -ne 'Complete' -or $Evidence.ScopeComplete -ne $true -or $Evidence.IdentitiesComplete -ne $true -or $Evidence.AdministratorsComplete -ne $true -or @($Evidence.Acls).Count -eq 0 -or $Evidence.NamespaceId -ne $Policy.NamespaceId -or -not $Evidence.Token -or $Evidence.Acls[-1].token -ne $Evidence.Token) { return $unknown } $identities = @{} foreach ($identity in $Evidence.Identities) { if (-not $identity.descriptor -or $null -eq $identity.isContainer -or $null -eq $identity.bypassMask -or $identities.ContainsKey($identity.descriptor)) { return $unknown } if ($identity.isContainer -and $null -eq $identity.members) { return $unknown } $identities[$identity.descriptor] = $identity } # Resolve nested groups once per principal. Cycles are valid; incomplete references are not. $memberships = @{} foreach ($identity in $Evidence.Identities) { foreach ($member in $identity.members) { if (-not $identities.ContainsKey([string]$member)) { return $unknown } if (-not $memberships.ContainsKey([string]$member)) { $memberships[[string]$member] = @() } $memberships[[string]$member] += $identity.descriptor } } $grants = @{} foreach ($acl in $Evidence.Acls) { if ($null -eq $acl.inheritPermissions -or $null -eq $acl.acesDictionary) { return $unknown } if (-not $acl.inheritPermissions) { $grants = @{} } $aces = if ($acl.acesDictionary -is [System.Collections.IDictionary]) { @($acl.acesDictionary.Values) } else { @($acl.acesDictionary.psobject.Properties.Value) } foreach ($ace in $aces) { if (-not $identities.ContainsKey([string]$ace.descriptor) -or $null -eq $ace.allow -or $null -eq $ace.deny) { return $unknown } $prior = $grants[$ace.descriptor] $explicit = [long]$ace.allow -bor [long]$ace.deny # Explicit entries override inherited entries for this identity and permission. $grants[$ace.descriptor] = @{ Allow = ([long]$prior.Allow -band (-bnot $explicit)) -bor [long]$ace.allow Deny = ([long]$prior.Deny -band (-bnot $explicit)) -bor [long]$ace.deny } } } $allowed = if ($Policy.Allowed -is [System.Collections.IDictionary]) { $Policy.Allowed } else { $map = @{}; foreach ($property in $Policy.Allowed.psobject.Properties) { $map[$property.Name] = $property.Value }; $map } foreach ($descriptor in $allowed.Keys) { if (-not $identities.ContainsKey([string]$descriptor)) { return $unknown } } $violations = @() foreach ($identity in $Evidence.Identities) { if ($identity.isContainer) { continue } $closure = @{} $pending = [System.Collections.Generic.Queue[string]]::new() $pending.Enqueue($identity.descriptor) while ($pending.Count -gt 0) { $descriptor = $pending.Dequeue() if ($closure.ContainsKey($descriptor)) { continue } $closure[$descriptor] = $true foreach ($group in $memberships[$descriptor]) { $pending.Enqueue($group) } } [long]$allow = 0; [long]$deny = 0; [long]$authorized = 0; [long]$bypass = 0 foreach ($descriptor in $closure.Keys) { $allow = $allow -bor [long]$grants[$descriptor].Allow $deny = $deny -bor [long]$grants[$descriptor].Deny $authorized = $authorized -bor [long]$allowed[$descriptor] $bypass = $bypass -bor [long]$identities[$descriptor].bypassMask } $effective = ($allow -band (-bnot $deny)) -bor $bypass $excess = $effective -band [long]$Policy.SensitiveMask -band (-bnot $authorized) if ($excess -ne 0) { $violations += [pscustomobject]@{ Descriptor = $identity.descriptor; Mask = $excess } } } if ($violations.Count -gt 0) { return [pscustomobject]@{ Outcome = 'Fail'; ReasonCode = 'UnauthorizedEffectivePermission'; Violations = $violations } } return [pscustomobject]@{ Outcome = 'Pass'; ReasonCode = 'EffectivePermissionsAuthorized'; Violations = @() } } |