From 39ba3c3ad77201ab5115f18ef8d292274d92c799 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sun, 23 Jun 2024 11:39:14 -0500 Subject: [PATCH] add: New process for collecting MgGraph output to make pester testing easier --- CHANGELOG.md | 4 + .../Get-AdminRoleUserAndAssignment.ps1 | 38 +++++++++ source/Private/Get-MgOutput.ps1 | 85 +++++++++++++++++++ .../Test-AdministrativeAccountCompliance.ps1 | 83 +++++++----------- source/tests/Test-GlobalAdminsCount.ps1 | 6 +- source/tests/Test-MailboxAuditingE3.ps1 | 9 +- source/tests/Test-MailboxAuditingE5.ps1 | 7 +- .../Test-ManagedApprovedPublicGroups.ps1 | 2 +- source/tests/Test-PasswordHashSync.ps1 | 2 +- source/tests/Test-RestrictTenantCreation.ps1 | 2 +- .../Get-AdminRoleUserAndAssignment.tests.ps1 | 27 ++++++ tests/Unit/Private/Get-MgOutput.tests.ps1 | 27 ++++++ 12 files changed, 224 insertions(+), 68 deletions(-) create mode 100644 source/Private/Get-AdminRoleUserAndAssignment.ps1 create mode 100644 source/Private/Get-MgOutput.ps1 create mode 100644 tests/Unit/Private/Get-AdminRoleUserAndAssignment.tests.ps1 create mode 100644 tests/Unit/Private/Get-MgOutput.tests.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 89e5f9e..54fe1b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,14 @@ The format is based on and uses the types of changes according to [Keep a Change ### Fixed - Fixed test 1.3.1 to include notification window for password expiration. +- Fixed 6.1.1 test definition to include the correct connection. ### Added - Added export to excel to `Export-M365SecurityAuditTable` function. +- `Get-AdminRoleUserLicense` function to get the license of a user with admin roles for 1.1.1. +- Skip MSOL connection confirmation to `Get-MFAStatus` function. +- Get-MgOutput function to get the output of the Microsoft Graph API per test and adjusted tests to utilize. ## [0.1.13] - 2024-06-18 diff --git a/source/Private/Get-AdminRoleUserAndAssignment.ps1 b/source/Private/Get-AdminRoleUserAndAssignment.ps1 new file mode 100644 index 0000000..2ed74ec --- /dev/null +++ b/source/Private/Get-AdminRoleUserAndAssignment.ps1 @@ -0,0 +1,38 @@ +function Get-AdminRoleUserAndAssignment { + [CmdletBinding()] + param () + + $result = @{} + + # Get the DisplayNames of all admin roles + $adminRoleNames = (Get-MgDirectoryRole | Where-Object { $null -ne $_.RoleTemplateId }).DisplayName + + # Get Admin Roles + $adminRoles = Get-MgRoleManagementDirectoryRoleDefinition | Where-Object { ($adminRoleNames -contains $_.DisplayName) -and ($_.DisplayName -ne "Directory Synchronization Accounts") } + + foreach ($role in $adminRoles) { + Write-Verbose "Processing role: $($role.DisplayName)" + $roleAssignments = Get-MgRoleManagementDirectoryRoleAssignment -Filter "roleDefinitionId eq '$($role.Id)'" + + foreach ($assignment in $roleAssignments) { + Write-Verbose "Processing role assignment for principal ID: $($assignment.PrincipalId)" + $userDetails = Get-MgUser -UserId $assignment.PrincipalId -Property "DisplayName, UserPrincipalName, Id, OnPremisesSyncEnabled" -ErrorAction SilentlyContinue + + if ($userDetails) { + Write-Verbose "Retrieved user details for: $($userDetails.UserPrincipalName)" + $licenses = Get-MgUserLicenseDetail -UserId $assignment.PrincipalId -ErrorAction SilentlyContinue + + if (-not $result[$role.DisplayName]) { + $result[$role.DisplayName] = @() + } + $result[$role.DisplayName] += [PSCustomObject]@{ + AssignmentId = $assignment.Id + UserDetails = $userDetails + Licenses = $licenses + } + } + } + } + + return $result +} diff --git a/source/Private/Get-MgOutput.ps1 b/source/Private/Get-MgOutput.ps1 new file mode 100644 index 0000000..b578883 --- /dev/null +++ b/source/Private/Get-MgOutput.ps1 @@ -0,0 +1,85 @@ +function Get-MgOutput { + <# + .SYNOPSIS + This is a sample Private function only visible within the module. + + .DESCRIPTION + This sample function is not exported to the module and only return the data passed as parameter. + + .EXAMPLE + $null = Get-MgOutput -PrivateData 'NOTHING TO SEE HERE' + + .PARAMETER PrivateData + The PrivateData parameter is what will be returned without transformation. + +#> + [cmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory = $true)] + [String] + $Rec + ) + + begin { + # Begin Block # + } + process { + switch ($rec) { + '1.1.3' { + # Step: Retrieve global admin role + $globalAdminRole = Get-MgDirectoryRole -Filter "RoleTemplateId eq '62e90394-69f5-4237-9190-012177145e10'" + # Step: Retrieve global admin members + $globalAdmins = Get-MgDirectoryRoleMember -DirectoryRoleId $globalAdminRole.Id + return $globalAdmins + } + '1.2.1' { + $allGroups = Get-MgGroup -All | Where-Object { $_.Visibility -eq "Public" } | Select-Object DisplayName, Visibility + return $allGroups + } + '5.1.2.3' { + # Retrieve the tenant creation policy + $tenantCreationPolicy = (Get-MgPolicyAuthorizationPolicy).DefaultUserRolePermissions | Select-Object AllowedToCreateTenants + return $tenantCreationPolicy + } + '5.1.8.1' { + # Retrieve password hash sync status (Condition A and C) + $passwordHashSync = Get-MgOrganization | Select-Object -ExpandProperty OnPremisesSyncEnabled + return $passwordHashSync + } + '6.1.2' { + $tenantSkus = Get-MgSubscribedSku -All + $e3SkuPartNumber = "SPE_E3" + $founde3Sku = $tenantSkus | Where-Object { $_.SkuPartNumber -eq $e3SkuPartNumber } + if ($founde3Sku.Count -ne 0) { + $allE3Users = Get-MgUser -Filter "assignedLicenses/any(x:x/skuId eq $($founde3Sku.SkuId) )" -All + return $allE3Users + } + else { + return $null + } + } + '6.1.3' { + $tenantSkus = Get-MgSubscribedSku -All + $e5SkuPartNumber = "SPE_E5" + $founde5Sku = $tenantSkus | Where-Object { $_.SkuPartNumber -eq $e5SkuPartNumber } + if ($founde5Sku.Count -ne 0) { + $allE5Users = Get-MgUser -Filter "assignedLicenses/any(x:x/skuId eq $($founde5Sku.SkuId) )" -All + return $allE5Users + } + else { + return $null + } + } + Default { + # 1.1.1 + $AdminRoleAssignmentsAndUsers = Get-AdminRoleUserAndAssignment + return $AdminRoleAssignmentsAndUsers + } + } + } + end { + Write-Verbose "Retuning data for Rec: $Rec" + } +} # end function Get-MgOutput + diff --git a/source/tests/Test-AdministrativeAccountCompliance.ps1 b/source/tests/Test-AdministrativeAccountCompliance.ps1 index e4644ad..cf0d390 100644 --- a/source/tests/Test-AdministrativeAccountCompliance.ps1 +++ b/source/tests/Test-AdministrativeAccountCompliance.ps1 @@ -1,76 +1,59 @@ function Test-AdministrativeAccountCompliance { [CmdletBinding()] - param ( - # Aligned - # Parameters can be added if needed - ) + param () begin { # The following conditions are checked: # Condition A: The administrative account is cloud-only (not synced). # Condition B: The account is assigned a valid license (e.g., Microsoft Entra ID P1 or P2). # Condition C: The administrative account does not have any other application assignments (only valid licenses). - $validLicenses = @('AAD_PREMIUM', 'AAD_PREMIUM_P2') $recnum = "1.1.1" Write-Verbose "Starting Test-AdministrativeAccountCompliance with Rec: $recnum" } process { - try { - # Retrieve all admin roles - Write-Verbose "Retrieving all admin roles" - # Get the DisplayNames of all admin roles - $adminRoleNames = (Get-MgDirectoryRole | Where-Object { $null -ne $_.RoleTemplateId }).DisplayName - # Use the DisplayNames to filter the roles in Get-MgRoleManagementDirectoryRoleDefinition - $adminRoles = Get-MgRoleManagementDirectoryRoleDefinition | Where-Object { ($adminRoleNames -contains $_.DisplayName) -and ($_.DisplayName -ne "Directory Synchronization Accounts")} + try { + # Retrieve admin roles, assignments, and user details including licenses + Write-Verbose "Retrieving admin roles, assignments, and user details including licenses" + $adminRoleAssignments = Get-MgOutput -Rec $recnum $adminRoleUsers = @() - # Loop through each admin role to get role assignments and user details - foreach ($role in $adminRoles) { - Write-Verbose "Processing role: $($role.DisplayName)" - $roleAssignments = Get-MgRoleManagementDirectoryRoleAssignment -Filter "roleDefinitionId eq '$($role.Id)'" + foreach ($roleName in $adminRoleAssignments.Keys) { + $assignments = $adminRoleAssignments[$roleName] + foreach ($assignment in $assignments) { + $userDetails = $assignment.UserDetails + $userId = $userDetails.Id + $userPrincipalName = $userDetails.UserPrincipalName + $licenses = $assignment.Licenses + $licenseString = if ($licenses) { ($licenses.SkuPartNumber -join '|') } else { "No Licenses Found" } - foreach ($assignment in $roleAssignments) { - Write-Verbose "Processing role assignment for principal ID: $($assignment.PrincipalId)" - # Get user details for each principal ID - $userDetails = Get-MgUser -UserId $assignment.PrincipalId -Property "DisplayName, UserPrincipalName, Id, OnPremisesSyncEnabled" -ErrorAction SilentlyContinue - if ($userDetails) { - Write-Verbose "Retrieved user details for: $($userDetails.UserPrincipalName)" - # Get user license details - $licenses = Get-MgUserLicenseDetail -UserId $assignment.PrincipalId -ErrorAction SilentlyContinue - $licenseString = if ($licenses) { ($licenses.SkuPartNumber -join '|') } else { "No Licenses Found" } + # Condition A: Check if the account is cloud-only + $cloudOnlyStatus = if ($userDetails.OnPremisesSyncEnabled) { "Fail" } else { "Pass" } - # Condition A: Check if the account is cloud-only - $cloudOnlyStatus = if ($userDetails.OnPremisesSyncEnabled) { "Fail" } else { "Pass" } + # Condition B: Check if the account has valid licenses + $hasValidLicense = $licenses.SkuPartNumber | ForEach-Object { $validLicenses -contains $_ } + $validLicensesStatus = if ($hasValidLicense) { "Pass" } else { "Fail" } - # Condition B: Check if the account has valid licenses - $hasValidLicense = $licenses.SkuPartNumber | ForEach-Object { $validLicenses -contains $_ } - $validLicensesStatus = if ($hasValidLicense) { "Pass" } else { "Fail" } + # Condition C: Check if the account has no other licenses + $hasInvalidLicense = $licenses.SkuPartNumber | ForEach-Object { $validLicenses -notcontains $_ } + $invalidLicenses = $licenses.SkuPartNumber | Where-Object { $validLicenses -notcontains $_ } + $applicationAssignmentStatus = if ($hasInvalidLicense) { "Fail" } else { "Pass" } - # Condition C: Check if the account has no other licenses - $hasInvalidLicense = $licenses.SkuPartNumber | ForEach-Object { $validLicenses -notcontains $_ } - $invalidLicenses = $licenses.SkuPartNumber | Where-Object { $validLicenses -notcontains $_ } - $applicationAssignmentStatus = if ($hasInvalidLicense) { "Fail" } else { "Pass" } + Write-Verbose "User: $userPrincipalName, Cloud-Only: $cloudOnlyStatus, Valid Licenses: $validLicensesStatus, Invalid Licenses: $($invalidLicenses -join ', ')" - Write-Verbose "User: $($userDetails.UserPrincipalName), Cloud-Only: $cloudOnlyStatus, Valid Licenses: $validLicensesStatus, Invalid Licenses: $($invalidLicenses -join ', ')" - - # Collect user information - $adminRoleUsers += [PSCustomObject]@{ - UserName = $userDetails.UserPrincipalName - RoleName = $role.DisplayName - UserId = $userDetails.Id - HybridUser = $userDetails.OnPremisesSyncEnabled - Licenses = $licenseString - CloudOnlyStatus = $cloudOnlyStatus - ValidLicensesStatus = $validLicensesStatus - ApplicationAssignmentStatus = $applicationAssignmentStatus - } - } - else { - Write-Verbose "No user details found for principal ID: $($assignment.PrincipalId)" + # Collect user information + $adminRoleUsers += [PSCustomObject]@{ + UserName = $userPrincipalName + RoleName = $roleName + UserId = $userId + HybridUser = $userDetails.OnPremisesSyncEnabled + Licenses = $licenseString + CloudOnlyStatus = $cloudOnlyStatus + ValidLicensesStatus = $validLicensesStatus + ApplicationAssignmentStatus = $applicationAssignmentStatus } } } diff --git a/source/tests/Test-GlobalAdminsCount.ps1 b/source/tests/Test-GlobalAdminsCount.ps1 index 865d02e..e52117a 100644 --- a/source/tests/Test-GlobalAdminsCount.ps1 +++ b/source/tests/Test-GlobalAdminsCount.ps1 @@ -30,11 +30,7 @@ function Test-GlobalAdminsCount { process { try { - # Step: Retrieve global admin role - $globalAdminRole = Get-MgDirectoryRole -Filter "RoleTemplateId eq '62e90394-69f5-4237-9190-012177145e10'" - - # Step: Retrieve global admin members - $globalAdmins = Get-MgDirectoryRoleMember -DirectoryRoleId $globalAdminRole.Id + $globalAdmins = Get-MgOutput -Rec $recnum # Step: Count the number of global admins $globalAdminCount = $globalAdmins.Count diff --git a/source/tests/Test-MailboxAuditingE3.ps1 b/source/tests/Test-MailboxAuditingE3.ps1 index 4f30450..33be83f 100644 --- a/source/tests/Test-MailboxAuditingE3.ps1 +++ b/source/tests/Test-MailboxAuditingE3.ps1 @@ -29,7 +29,6 @@ function Test-MailboxAuditingE3 { # Dot source the class script if necessary #. .\source\Classes\CISAuditResult.ps1 - $e3SkuPartNumber = "SPE_E3" $actionDictionaries = Get-Action -Dictionaries # E3 specific actions @@ -38,14 +37,14 @@ function Test-MailboxAuditingE3 { $OwnerActions = $actionDictionaries.OwnerActions.Keys | Where-Object { $_ -notin @("MailItemsAccessed", "Send") } $allFailures = @() - $founde3Sku = Get-MgSubscribedSku -All | Where-Object { $_.SkuPartNumber -eq $e3SkuPartNumber } - $processedUsers = @{} # Dictionary to track processed users $recnum = "6.1.2" + $allUsers = Get-MgOutput -Rec $recnum + $processedUsers = @{} # Dictionary to track processed users + } process { - if ($founde3Sku.Count -ne 0) { - $allUsers = Get-MgUser -Filter "assignedLicenses/any(x:x/skuId eq $($founde3Sku.SkuId) )" -All + if ($null -ne $allUsers) { $mailboxes = Get-EXOMailbox -PropertySets Audit try { foreach ($user in $allUsers) { diff --git a/source/tests/Test-MailboxAuditingE5.ps1 b/source/tests/Test-MailboxAuditingE5.ps1 index adca387..24adda4 100644 --- a/source/tests/Test-MailboxAuditingE5.ps1 +++ b/source/tests/Test-MailboxAuditingE5.ps1 @@ -27,9 +27,6 @@ function Test-MailboxAuditingE5 { # - Condition C: AuditDelegate actions do not include all of the following: ApplyRecord, Create, HardDelete, MailItemsAccessed, MoveToDeletedItems, SendAs, SendOnBehalf, SoftDelete, Update, UpdateFolderPermissions, UpdateInboxRules. # - Condition D: AuditOwner actions do not include all of the following: ApplyRecord, HardDelete, MailItemsAccessed, MoveToDeletedItems, Send, SoftDelete, Update, UpdateCalendarDelegation, UpdateFolderPermissions, UpdateInboxRules. - $e5SkuPartNumber = "SPE_E5" - $founde5Sku = Get-MgSubscribedSku -All | Where-Object { $_.SkuPartNumber -eq $e5SkuPartNumber } - $actionDictionaries = Get-Action -Dictionaries $AdminActions = $actionDictionaries.AdminActions.Keys $DelegateActions = $actionDictionaries.DelegateActions.Keys @@ -38,11 +35,11 @@ function Test-MailboxAuditingE5 { $allFailures = @() $processedUsers = @{} $recnum = "6.1.3" + $allUsers = Get-MgOutput -Rec $recnum } process { - if (($founde5Sku.count) -ne 0) { - $allUsers = Get-MgUser -Filter "assignedLicenses/any(x:x/skuId eq $($founde5Sku.SkuId) )" -All + if ($null -ne $allUsers) { $mailboxes = Get-EXOMailbox -PropertySets Audit try { foreach ($user in $allUsers) { diff --git a/source/tests/Test-ManagedApprovedPublicGroups.ps1 b/source/tests/Test-ManagedApprovedPublicGroups.ps1 index 829f202..0563ed5 100644 --- a/source/tests/Test-ManagedApprovedPublicGroups.ps1 +++ b/source/tests/Test-ManagedApprovedPublicGroups.ps1 @@ -30,7 +30,7 @@ function Test-ManagedApprovedPublicGroups { process { try { # Step: Retrieve all groups with visibility set to 'Public' - $allGroups = Get-MgGroup -All | Where-Object { $_.Visibility -eq "Public" } | Select-Object DisplayName, Visibility + $allGroups = Get-MgOutput -Rec $recnum # Step: Determine failure reasons based on the presence of public groups $failureReasons = if ($null -ne $allGroups -and $allGroups.Count -gt 0) { diff --git a/source/tests/Test-PasswordHashSync.ps1 b/source/tests/Test-PasswordHashSync.ps1 index 1d8362e..8c1e1b1 100644 --- a/source/tests/Test-PasswordHashSync.ps1 +++ b/source/tests/Test-PasswordHashSync.ps1 @@ -34,7 +34,7 @@ function Test-PasswordHashSync { # 5.1.8.1 (L1) Ensure password hash sync is enabled for hybrid deployments # Retrieve password hash sync status (Condition A and C) - $passwordHashSync = Get-MgOrganization | Select-Object -ExpandProperty OnPremisesSyncEnabled + $passwordHashSync = Get-MgOutput -Rec $recnum $hashSyncResult = $passwordHashSync # Prepare failure reasons and details based on compliance diff --git a/source/tests/Test-RestrictTenantCreation.ps1 b/source/tests/Test-RestrictTenantCreation.ps1 index 6d3c314..c1a7c4e 100644 --- a/source/tests/Test-RestrictTenantCreation.ps1 +++ b/source/tests/Test-RestrictTenantCreation.ps1 @@ -35,7 +35,7 @@ function Test-RestrictTenantCreation { # 5.1.2.3 (L1) Ensure 'Restrict non-admin users from creating tenants' is set to 'Yes' # Retrieve the tenant creation policy - $tenantCreationPolicy = (Get-MgPolicyAuthorizationPolicy).DefaultUserRolePermissions | Select-Object AllowedToCreateTenants + $tenantCreationPolicy = Get-MgOutput -Rec $recnum $tenantCreationResult = -not $tenantCreationPolicy.AllowedToCreateTenants # Prepare failure reasons and details based on compliance diff --git a/tests/Unit/Private/Get-AdminRoleUserAndAssignment.tests.ps1 b/tests/Unit/Private/Get-AdminRoleUserAndAssignment.tests.ps1 new file mode 100644 index 0000000..4a2aa69 --- /dev/null +++ b/tests/Unit/Private/Get-AdminRoleUserAndAssignment.tests.ps1 @@ -0,0 +1,27 @@ +$ProjectPath = "$PSScriptRoot\..\..\.." | Convert-Path +$ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{ + ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and + $(try { Test-ModuleManifest $_.FullName -ErrorAction Stop } catch { $false } ) + }).BaseName + + +Import-Module $ProjectName + +InModuleScope $ProjectName { + Describe Get-PrivateFunction { + Context 'Default' { + BeforeEach { + $return = Get-PrivateFunction -PrivateData 'string' + } + + It 'Returns a single object' { + ($return | Measure-Object).Count | Should -Be 1 + } + + It 'Returns a string based on the parameter PrivateData' { + $return | Should -Be 'string' + } + } + } +} + diff --git a/tests/Unit/Private/Get-MgOutput.tests.ps1 b/tests/Unit/Private/Get-MgOutput.tests.ps1 new file mode 100644 index 0000000..4a2aa69 --- /dev/null +++ b/tests/Unit/Private/Get-MgOutput.tests.ps1 @@ -0,0 +1,27 @@ +$ProjectPath = "$PSScriptRoot\..\..\.." | Convert-Path +$ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{ + ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and + $(try { Test-ModuleManifest $_.FullName -ErrorAction Stop } catch { $false } ) + }).BaseName + + +Import-Module $ProjectName + +InModuleScope $ProjectName { + Describe Get-PrivateFunction { + Context 'Default' { + BeforeEach { + $return = Get-PrivateFunction -PrivateData 'string' + } + + It 'Returns a single object' { + ($return | Measure-Object).Count | Should -Be 1 + } + + It 'Returns a string based on the parameter PrivateData' { + $return | Should -Be 'string' + } + } + } +} +