Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add privileged service principals table to AAD report #1467

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
20 changes: 20 additions & 0 deletions PowerShell/ScubaGear/Modules/CreateReport/CreateReport.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -270,13 +270,33 @@ function New-Report {
# Create a section header for the licensing information
$LicensingHTML = "<h2>Tenant Licensing Information</h2>" + $LicenseTable

# Create a section for privileged service principals
$privilegedServicePrincipalsTable = $SettingsExport.privileged_service_principals.psobject.properties | ForEach-Object {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic in AADProvider:

If $PrivilegedServicePrincipals is $null, then it equals {}. Otherwise, set equal to $PrivilegedServicePrincipals
Then in the AADProvider json set "privileged_service_principals" equal to $PrivilegedServicePrincipals

If "privileged_service_principals" = {}, then $principal.DisplayName, $principal.ServicePrincipalId, etc. are nonexistent properties. Can we add some error handling here?

We could check $SettingsExport.privileged_service_principals.PSObject.Count > 0 or some other condition.

$principal = $_.Value
[pscustomobject]@{
"Display Name" = $principal.DisplayName
"Service Principal ID" = $principal.ServicePrincipalId
"Roles" = ($principal.roles -join ", ")
"App ID" = $principal.AppId

}
} | ConvertTo-Html -Fragment

$privilegedServicePrincipalsTable = $privilegedServicePrincipalsTable -replace '^(.*?)<table>', '<table id="privileged-service-principals" style="text-align:center;">'

# Create a section header for the service principal information
$privilegedServicePrincipalsTableHTML = "<h2>Privileged Service Principal Table</h2>" + $privilegedServicePrincipalsTable


$ReportHTML = $ReportHTML.Replace("{AADWARNING}", $AADWarning)
$ReportHTML = $ReportHTML.Replace("{LICENSING_INFO}", $LicensingHTML)
$ReportHTML = $ReportHTML.Replace("{SERVICE_PRINCIPAL}", $privilegedServicePrincipalsTableHTML)
$CapJson = ConvertTo-Json $SettingsExport.cap_table_data
}
else {
$ReportHTML = $ReportHTML.Replace("{AADWARNING}", $NoWarning)
$ReportHTML = $ReportHTML.Replace("{LICENSING_INFO}", "")
$ReportHTML = $ReportHTML.Replace("{SERVICE_PRINCIPAL}", "")
$CapJson = "null"
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ <h1>{TITLE}</h1>
<h4>{AADWARNING}</h4>
{TABLES}
{LICENSING_INFO}
{SERVICE_PRINCIPAL}
</main>
</body>
</html>
171 changes: 162 additions & 9 deletions PowerShell/ScubaGear/Modules/Providers/ExportAADProvider.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -111,21 +111,37 @@ function Export-AADProvider {
# The RequiredServicePlan variable is used so that PIM Cmdlets are only executed if the tenant has the premium license
$RequiredServicePlan = $ServicePlans | Where-Object -Property ServicePlanName -eq -Value "AAD_PREMIUM_P2"

# Get-PrivilegedUser provides a list of privileged users and their role assignments.
if ($RequiredServicePlan) {
# If the tenant has the premium license then we also include calls to PIM APIs
$PrivilegedUsers = $Tracker.TryCommand("Get-PrivilegedUser", @{"TenantHasPremiumLicense"=$true; "M365Environment"=$M365Environment})
$PrivilegedObjects = $Tracker.TryCommand("Get-PrivilegedUser", @{"TenantHasPremiumLicense"=$true; "M365Environment"=$M365Environment})
}
else{
$PrivilegedUsers = $Tracker.TryCommand("Get-PrivilegedUser", @{"TenantHasPremiumLicense"=$false; "M365Environment"=$M365Environment})
else {
$PrivilegedObjects = $Tracker.TryCommand("Get-PrivilegedUser", @{"TenantHasPremiumLicense"=$false; "M365Environment"=$M365Environment})
}
# The Converto-Json call below doesn't need to have the input wrapped in an
# array (e.g, "ConvertTo-Json (@PrivilegedUsers)") because $PrivilegedUsers is
# a dictionary, not an array, and ConvertTo-Json doesn't mess up dictionaries like it does arrays
$PrivilegedUsers = $PrivilegedUsers | ConvertTo-Json

# # Split the objects into users and service principals
$PrivilegedUsers = @{}
$PrivilegedServicePrincipals = @{}

#PrivilegedObjects is an array because of the tracker.trycommand, and so the first index is the hashtable
foreach ($key in $PrivilegedObjects[0].Keys) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we certain that $PrivilegedObjects will always contain at least one entry? Otherwise accessing the 0th index may throw an error if an entry does not exist.

Either way, I also think it would help to see some error handling to check if $PrivilegedObjects[0].Keys is not null prior to entering the loop.


# Check if it has ServicePrincipalId property instead of AppId
if ($null -ne $PrivilegedObjects[0][$key].ServicePrincipalId) {
$PrivilegedServicePrincipals[$key] = $PrivilegedObjects[0][$key]
}
else {
$PrivilegedUsers[$key] = $PrivilegedObjects[0][$key]
}
}

$PrivilegedUsers = ConvertTo-Json $PrivilegedUsers
$PrivilegedServicePrincipals = ConvertTo-Json $PrivilegedServicePrincipals

# While ConvertTo-Json won't mess up a dict as described in the above comment,
# on error, $TryCommand returns an empty list, not a dictionary.
$PrivilegedUsers = if ($null -eq $PrivilegedUsers) {"{}"} else {$PrivilegedUsers}
$PrivilegedServicePrincipals = if ($null -eq $PrivilegedServicePrincipals) {"{}"} else {$PrivilegedServicePrincipals}

# Get-PrivilegedRole provides a list of security configurations for each privileged role and information about Active user assignments
if ($RequiredServicePlan){
Expand Down Expand Up @@ -188,6 +204,7 @@ function Export-AADProvider {
"cap_table_data": $CapTableData,
"authorization_policies": $AuthZPolicies,
"privileged_users": $PrivilegedUsers,
"privileged_service_principals": $PrivilegedServicePrincipals,
"privileged_roles": $PrivilegedRoles,
"service_plans": $ServicePlans,
"directory_settings": $DirectorySettings,
Expand Down Expand Up @@ -274,6 +291,9 @@ function Get-PrivilegedUser {
if ($Objecttype -eq "user") {
LoadObjectDataIntoPrivilegedUserHashtable -RoleName $Role.DisplayName -PrivilegedUsers $PrivilegedUsers -ObjectId $User.Id -TenantHasPremiumLicense $TenantHasPremiumLicense -M365Environment $M365Environment -Objecttype "user"
}
elseif ($Objecttype -eq "servicePrincipal") {
LoadObjectDataIntoPrivilegedUserHashtable -RoleName $Role.DisplayName -PrivilegedUsers $PrivilegedUsers -ObjectId $User.Id -TenantHasPremiumLicense $TenantHasPremiumLicense -M365Environment $M365Environment -Objecttype "serviceprincipal"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a new unit test to handle $Objecttype="serviceprincipal" in LoadObjectDataIntoPrivilegedUserHashTable.Tests.ps1?

}
elseif ($Objecttype -eq "group") {
# In this context $User.Id is a group identifier
$GroupId = $User.Id
Expand Down Expand Up @@ -391,6 +411,23 @@ function LoadObjectDataIntoPrivilegedUserHashtable {
}
}

elseif ($Objecttype -eq "serviceprincipal") {

# In this section we need to add the service principal information to the "service principal" hashtable
if (-Not $PrivilegedUsers.ContainsKey($ObjectId)) {
$AADServicePrincipal = Get-MgBetaServicePrincipal -ServicePrincipalId $ObjectId -ErrorAction Stop
$PrivilegedUsers[$ObjectId] = @{
"DisplayName" = $AADServicePrincipal.DisplayName
"ServicePrincipalId" = $AADServicePrincipal.Id
"AppId" = $AADServicePrincipal.AppId
"roles" = @()
}
}
if ($PrivilegedUsers[$ObjectId].roles -notcontains $RoleName) {
$PrivilegedUsers[$ObjectId].roles += $RoleName
}
}

elseif ($Objecttype -eq "group") {
# In this context $ObjectId is a group identifier so we need to iterate the group members
$GroupId = $ObjectId
Expand All @@ -411,6 +448,22 @@ function LoadObjectDataIntoPrivilegedUserHashtable {
$PrivilegedUsers[$GroupMember.Id].roles += $RoleName
}
}
elseif ($Membertype -eq "serviceprincipal") {

# In this section we need to add the service principal information to the "service principal" hashtable
if (-Not $PrivilegedUsers.ContainsKey($GroupMember.Id)) {
$AADServicePrincipal = Get-MgBetaServicePrincipal -ServicePrincipalId $GroupMember.Id -ErrorAction Stop
$PrivilegedUsers[$GroupMember.Id] = @{
"DisplayName" = $AADServicePrincipal.DisplayName
"ServicePrincipalId" = $AADServicePrincipal.Id
"AppId" = $AADServicePrincipal.AppId
"roles" = @()
}
}
if ($PrivilegedUsers[$GroupMember.Id].roles -notcontains $RoleName) {
$PrivilegedUsers[$GroupMember.Id].roles += $RoleName
}
}
}

# Since this is a group, we need to also process assignments in PIM in case it is in PIM for Groups
Expand All @@ -437,7 +490,107 @@ function LoadObjectDataIntoPrivilegedUserHashtable {
}

}

# function LoadObjectDataIntoPrivilegedUserHashtable {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove comment blocks

# param (
# [Parameter(Mandatory=$true)]
# [ValidateNotNullOrEmpty()]
# [string]$RoleName,

# [Parameter(Mandatory=$true)]
# [hashtable]$PrivilegedUsers,

# [Parameter(Mandatory=$true)]
# [ValidateNotNullOrEmpty()]
# [string]$ObjectId,

# [Parameter(Mandatory=$true)]
# [ValidateNotNullOrEmpty()]
# [bool]$TenantHasPremiumLicense,

# [Parameter(Mandatory=$true)]
# [ValidateNotNullOrEmpty()]
# [string]$M365Environment,

# [Parameter()]
# [string]$Objecttype = "",

# [Parameter()]
# [int]$Recursioncount = 0
# )

# if ($recursioncount -ge 2) {
# return
# }

# if ($Objecttype -eq "") {
# try {
# $DirectoryObject = Get-MgBetaDirectoryObject -ErrorAction Stop -DirectoryObjectId $ObjectId
# } catch {
# if ($_.Exception.Message -match "Request_ResourceNotFound") {
# Write-Warning "Processing privileged users. Resource $ObjectId may have been recently deleted from the directory because it was not found."
# return
# }
# else {
# throw $_
# }
# }
# $Objecttype = $DirectoryObject.AdditionalProperties."@odata.type" -replace "#microsoft.graph."
# }

# if ($Objecttype -eq "user") {
# if (-Not $PrivilegedUsers.ContainsKey($ObjectId)) {
# $AADUser = Get-MgBetaUser -ErrorAction Stop -UserId $ObjectId
# $PrivilegedUsers[$ObjectId] = @{"DisplayName"=$AADUser.DisplayName; "OnPremisesImmutableId"=$AADUser.OnPremisesImmutableId; "roles"=@()}
# }
# if ($PrivilegedUsers[$ObjectId].roles -notcontains $RoleName) {
# $PrivilegedUsers[$ObjectId].roles += $RoleName
# }
# }
# elseif ($Objecttype -eq "servicePrincipal") {
# if (-Not $PrivilegedUsers.ContainsKey($ObjectId)) {
# $AADServicePrincipal = Get-MgBetaServicePrincipal -ServicePrincipalId $ObjectId -ErrorAction Stop
# $PrivilegedUsers[$ObjectId] = @{
# "DisplayName" = $AADServicePrincipal.DisplayName
# "ServicePrincipalId" = $AADServicePrincipal.Id
# "AppId" = $AADServicePrincipal.AppId
# "roles" = @()
# }
# }
# if ($PrivilegedUsers[$ObjectId].roles -notcontains $RoleName) {
# $PrivilegedUsers[$ObjectId].roles += $RoleName
# }
# }
# elseif ($Objecttype -eq "group") {
# $GroupId = $ObjectId
# $GroupMembers = Get-MgBetaGroupMember -All -ErrorAction Stop -GroupId $GroupId

# foreach ($GroupMember in $GroupMembers) {
# $Membertype = $GroupMember.AdditionalProperties."@odata.type" -replace "#microsoft.graph."
# if ($Membertype -eq "user" -or $Membertype -eq "servicePrincipal") {
# if (-Not $PrivilegedUsers.ContainsKey($GroupMember.Id)) {
# LoadObjectDataIntoPrivilegedUserHashtable -RoleName $RoleName -PrivilegedUsers $PrivilegedUsers -ObjectId $GroupMember.Id -TenantHasPremiumLicense $TenantHasPremiumLicense -M365Environment $M365Environment -Objecttype $Membertype
# }
# if ($PrivilegedUsers[$GroupMember.Id].roles -notcontains $RoleName) {
# $PrivilegedUsers[$GroupMember.Id].roles += $RoleName
# }
# }
# }

# if ($TenantHasPremiumLicense) {
# $graphArgs = @{
# "commandlet" = "Get-MgBetaIdentityGovernancePrivilegedAccessGroupEligibilityScheduleInstance"
# "queryParams" = @{'$filter' = "groupId eq '$GroupId'"}
# "M365Environment" = $M365Environment }
# $PIMGroupMembers = Invoke-GraphDirectly @graphArgs
# foreach ($GroupMember in $PIMGroupMembers) {
# if ($GroupMember.AccessId -ne "member") { continue }
# $PIMEligibleUserId = $GroupMember.PrincipalId
# $LoopIterationRecursioncount = $Recursioncount + 1
# LoadObjectDataIntoPrivilegedUserHashtable -RoleName $RoleName -PrivilegedUsers $PrivilegedUsers -ObjectId $PIMEligibleUserId -TenantHasPremiumLicense $TenantHasPremiumLicense -M365Environment $M365Environment -Recursioncount $LoopIterationRecursioncount
# }
# }
# }
# }
function AddRuleSource{
<#
.NOTES
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Import-Module -Name $PSScriptRoot/../ExportEXOProvider.psm1 -Function Get-ScubaSpfRecord, Get-ScubaDkimRecord, Get-ScubaDmarcRecord
Import-Module -Name $PSScriptRoot/../ExportAADProvider.psm1 -Function Get-PrivilegedRole, Get-PrivilegedUser
Import-Module -Name $PSScriptRoot/../ExportAADProvider.psm1 -Function Get-PrivilegedRole, Get-PrivilegedUser, Get-PrivilegedServicePrincipal
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see Get-PrivilegedServicePrincipal as a function in your branch's ExportAADProvider.psm1.


class CommandTracker {
[string[]]$SuccessfulCommands = @()
Expand Down
5 changes: 5 additions & 0 deletions PowerShell/ScubaGear/RequiredVersions.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,10 @@ $ModuleList = @(
ModuleName = 'powershell-yaml'
ModuleVersion = [version] '0.4.2'
MaximumVersion = [version] '0.99.99999'
},
@{
ModuleName = 'Microsoft.Graph.Beta.Applications'
ModuleVersion = [version] '2.0.0'
MaximumVersion = [version] '2.99.99999'
}
)
Loading