diff --git a/source/Private/Assert-ModuleAvailability.ps1 b/source/Private/Assert-ModuleAvailability.ps1 index a0f2f33..807594f 100644 --- a/source/Private/Assert-ModuleAvailability.ps1 +++ b/source/Private/Assert-ModuleAvailability.ps1 @@ -1,37 +1,40 @@ function Assert-ModuleAvailability { + [CmdletBinding()] [OutputType([void]) ] param( [string]$ModuleName, [string]$RequiredVersion, [string[]]$SubModules = @() ) + process { + try { + $module = Get-Module -ListAvailable -Name $ModuleName | Where-Object { $_.Version -ge [version]$RequiredVersion } + if ($null -eq $module) { + Write-Verbose "Installing $ModuleName module..." + Install-Module -Name $ModuleName -RequiredVersion $RequiredVersion -Force -AllowClobber -Scope CurrentUser | Out-Null + } + elseif ($module.Version -lt [version]$RequiredVersion) { + Write-Verbose "Updating $ModuleName module to required version..." + Update-Module -Name $ModuleName -RequiredVersion $RequiredVersion -Force | Out-Null + } + else { + Write-Verbose "$ModuleName module is already at required version or newer." + } + if ($SubModules.Count -gt 0) { + foreach ($subModule in $SubModules) { + Write-Verbose "Importing submodule $ModuleName.$subModule..." + Get-Module "$ModuleName.$subModule" | Import-Module -RequiredVersion $RequiredVersion -ErrorAction Stop | Out-Null + } + } + else { + Write-Verbose "Importing module $ModuleName..." + Import-Module -Name $ModuleName -RequiredVersion $RequiredVersion -ErrorAction Stop -WarningAction SilentlyContinue | Out-Null + } - try { - $module = Get-Module -ListAvailable -Name $ModuleName | Where-Object { $_.Version -ge [version]$RequiredVersion } - - if ($null -eq $module) { - Write-Host "Installing $ModuleName module..." -ForegroundColor Yellow - Install-Module -Name $ModuleName -RequiredVersion $RequiredVersion -Force -AllowClobber -Scope CurrentUser | Out-Null } - elseif ($module.Version -lt [version]$RequiredVersion) { - Write-Host "Updating $ModuleName module to required version..." -ForegroundColor Yellow - Update-Module -Name $ModuleName -RequiredVersion $RequiredVersion -Force | Out-Null - } - else { - Write-Host "$ModuleName module is already at required version or newer." -ForegroundColor Gray - } - - if ($SubModules.Count -gt 0) { - foreach ($subModule in $SubModules) { - Write-Host "Importing submodule $ModuleName.$subModule..." -ForegroundColor DarkGray - Import-Module -Name "$ModuleName.$subModule" -RequiredVersion $RequiredVersion -ErrorAction Stop | Out-Null - } - } else { - Write-Host "Importing module $ModuleName..." -ForegroundColor DarkGray - Import-Module -Name $ModuleName -RequiredVersion $RequiredVersion -ErrorAction Stop -WarningAction SilentlyContinue | Out-Null + catch { + throw "Assert-ModuleAvailability:`n$_" } } - catch { - Write-Warning "An error occurred with module $ModuleName`: $_" - } + } diff --git a/source/Private/Connect-M365Suite.ps1 b/source/Private/Connect-M365Suite.ps1 index 54ec3cf..b429357 100644 --- a/source/Private/Connect-M365Suite.ps1 +++ b/source/Private/Connect-M365Suite.ps1 @@ -11,14 +11,18 @@ function Connect-M365Suite { [Parameter(Mandatory = $false)] [switch]$SkipConfirmation ) - - $VerbosePreference = "SilentlyContinue" + if (!$SkipConfirmation) { + $VerbosePreference = "Continue" + } + else { + $VerbosePreference = "SilentlyContinue" + } $tenantInfo = @() $connectedServices = @() try { if ($RequiredConnections -contains "AzureAD" -or $RequiredConnections -contains "AzureAD | EXO" -or $RequiredConnections -contains "AzureAD | EXO | Microsoft Graph") { - Write-Host "Connecting to Azure Active Directory..." -ForegroundColor Yellow + Write-Verbose "Connecting to Azure Active Directory..." Connect-AzureAD -WarningAction SilentlyContinue | Out-Null $tenantDetails = Get-AzureADTenantDetail -WarningAction SilentlyContinue $tenantInfo += [PSCustomObject]@{ @@ -27,11 +31,11 @@ function Connect-M365Suite { TenantID = $tenantDetails.ObjectId } $connectedServices += "AzureAD" - Write-Host "Successfully connected to Azure Active Directory." -ForegroundColor Green + Write-Verbose "Successfully connected to Azure Active Directory." } if ($RequiredConnections -contains "Microsoft Graph" -or $RequiredConnections -contains "EXO | Microsoft Graph") { - Write-Host "Connecting to Microsoft Graph with scopes: Directory.Read.All, Domain.Read.All, Policy.Read.All, Organization.Read.All" -ForegroundColor Yellow + Write-Verbose "Connecting to Microsoft Graph with scopes: Directory.Read.All, Domain.Read.All, Policy.Read.All, Organization.Read.All" try { Connect-MgGraph -Scopes "Directory.Read.All", "Domain.Read.All", "Policy.Read.All", "Organization.Read.All" -NoWelcome | Out-Null $graphOrgDetails = Get-MgOrganization @@ -41,10 +45,10 @@ function Connect-M365Suite { TenantID = $graphOrgDetails.Id } $connectedServices += "Microsoft Graph" - Write-Host "Successfully connected to Microsoft Graph with specified scopes." -ForegroundColor Green + Write-Verbose "Successfully connected to Microsoft Graph with specified scopes." } catch { - Write-Host "Failed to connect to MgGraph, attempting device auth." -ForegroundColor Yellow + Write-Verbose "Failed to connect to MgGraph, attempting device auth." Connect-MgGraph -Scopes "Directory.Read.All", "Domain.Read.All", "Policy.Read.All", "Organization.Read.All" -UseDeviceCode -NoWelcome | Out-Null $graphOrgDetails = Get-MgOrganization $tenantInfo += [PSCustomObject]@{ @@ -53,12 +57,12 @@ function Connect-M365Suite { TenantID = $graphOrgDetails.Id } $connectedServices += "Microsoft Graph" - Write-Host "Successfully connected to Microsoft Graph with specified scopes." -ForegroundColor Green + Write-Verbose "Successfully connected to Microsoft Graph with specified scopes." } } if ($RequiredConnections -contains "EXO" -or $RequiredConnections -contains "AzureAD | EXO" -or $RequiredConnections -contains "Microsoft Teams | EXO" -or $RequiredConnections -contains "EXO | Microsoft Graph") { - Write-Host "Connecting to Exchange Online..." -ForegroundColor Yellow + Write-Verbose "Connecting to Exchange Online..." Connect-ExchangeOnline -ShowBanner:$false | Out-Null $exoTenant = (Get-OrganizationConfig).Identity $tenantInfo += [PSCustomObject]@{ @@ -67,11 +71,11 @@ function Connect-M365Suite { TenantID = "N/A" } $connectedServices += "EXO" - Write-Host "Successfully connected to Exchange Online." -ForegroundColor Green + Write-Verbose "Successfully connected to Exchange Online." } if ($RequiredConnections -contains "SPO") { - Write-Host "Connecting to SharePoint Online..." -ForegroundColor Yellow + Write-Verbose "Connecting to SharePoint Online..." Connect-SPOService -Url $TenantAdminUrl | Out-Null $spoContext = Get-SPOCrossTenantHostUrl $tenantName = Get-UrlLine -Output $spoContext @@ -80,11 +84,11 @@ function Connect-M365Suite { TenantName = $tenantName } $connectedServices += "SPO" - Write-Host "Successfully connected to SharePoint Online." -ForegroundColor Green + Write-Verbose "Successfully connected to SharePoint Online." } if ($RequiredConnections -contains "Microsoft Teams" -or $RequiredConnections -contains "Microsoft Teams | EXO") { - Write-Host "Connecting to Microsoft Teams..." -ForegroundColor Yellow + Write-Verbose "Connecting to Microsoft Teams..." Connect-MicrosoftTeams | Out-Null $teamsTenantDetails = Get-CsTenant $tenantInfo += [PSCustomObject]@{ @@ -93,20 +97,20 @@ function Connect-M365Suite { TenantID = $teamsTenantDetails.TenantId } $connectedServices += "Microsoft Teams" - Write-Host "Successfully connected to Microsoft Teams." -ForegroundColor Green + Write-Verbose "Successfully connected to Microsoft Teams." } # Display tenant information and confirm with the user if (-not $SkipConfirmation) { - Write-Host "Connected to the following tenants:" -ForegroundColor Yellow + Write-Verbose "Connected to the following tenants:" foreach ($tenant in $tenantInfo) { - Write-Host "Service: $($tenant.Service)" -ForegroundColor Cyan - Write-Host "Tenant Context: $($tenant.TenantName)`n" -ForegroundColor Green - #Write-Host "Tenant ID: $($tenant.TenantID)" + Write-Verbose "Service: $($tenant.Service)" + Write-Verbose "Tenant Context: $($tenant.TenantName)`n" + #Write-Verbose "Tenant ID: $($tenant.TenantID)" } $confirmation = Read-Host "Do you want to proceed with these connections? (Y/N)" if ($confirmation -notlike 'Y') { - Write-Host "Connection setup aborted by user." -ForegroundColor Red + Write-Verbose "Connection setup aborted by user." Disconnect-M365Suite -RequiredConnections $connectedServices throw "User aborted connection setup." } @@ -114,7 +118,7 @@ function Connect-M365Suite { } catch { $VerbosePreference = "Continue" - Write-Host "There was an error establishing one or more connections: $_" -ForegroundColor Red + Write-Verbose "There was an error establishing one or more connections: $_" throw $_ } diff --git a/source/Private/Disconnect-M365Suite.ps1 b/source/Private/Disconnect-M365Suite.ps1 index b338f67..6cbfff2 100644 --- a/source/Private/Disconnect-M365Suite.ps1 +++ b/source/Private/Disconnect-M365Suite.ps1 @@ -8,7 +8,7 @@ function Disconnect-M365Suite { # Clean up sessions try { if ($RequiredConnections -contains "EXO" -or $RequiredConnections -contains "AzureAD | EXO" -or $RequiredConnections -contains "Microsoft Teams | EXO") { - Write-Host "Disconnecting from Exchange Online..." -ForegroundColor Green + Write-Verbose "Disconnecting from Exchange Online..." Disconnect-ExchangeOnline -Confirm:$false | Out-Null } } @@ -18,7 +18,7 @@ function Disconnect-M365Suite { try { if ($RequiredConnections -contains "AzureAD" -or $RequiredConnections -contains "AzureAD | EXO") { - Write-Host "Disconnecting from Azure AD..." -ForegroundColor Green + Write-Verbose "Disconnecting from Azure AD..." Disconnect-AzureAD | Out-Null } } @@ -28,7 +28,7 @@ function Disconnect-M365Suite { try { if ($RequiredConnections -contains "Microsoft Graph") { - Write-Host "Disconnecting from Microsoft Graph..." -ForegroundColor Green + Write-Verbose "Disconnecting from Microsoft Graph..." Disconnect-MgGraph | Out-Null } } @@ -38,7 +38,7 @@ function Disconnect-M365Suite { try { if ($RequiredConnections -contains "SPO") { - Write-Host "Disconnecting from SharePoint Online..." -ForegroundColor Green + Write-Verbose "Disconnecting from SharePoint Online..." Disconnect-SPOService | Out-Null } } @@ -48,13 +48,12 @@ function Disconnect-M365Suite { try { if ($RequiredConnections -contains "Microsoft Teams" -or $RequiredConnections -contains "Microsoft Teams | EXO") { - Write-Host "Disconnecting from Microsoft Teams..." -ForegroundColor Green + Write-Verbose "Disconnecting from Microsoft Teams..." Disconnect-MicrosoftTeams | Out-Null } } catch { Write-Warning "Failed to disconnect from Microsoft Teams: $_" } - - Write-Host "All necessary sessions have been disconnected." -ForegroundColor Green + Write-Verbose "All necessary sessions have been disconnected." } \ No newline at end of file diff --git a/source/Private/Get-RequiredModule.ps1 b/source/Private/Get-RequiredModule.ps1 index e3b410a..ac8f3fb 100644 --- a/source/Private/Get-RequiredModule.ps1 +++ b/source/Private/Get-RequiredModule.ps1 @@ -14,7 +14,7 @@ function Get-RequiredModule { return @( @{ ModuleName = "ExchangeOnlineManagement"; RequiredVersion = "3.3.0"; SubModules = @() }, @{ ModuleName = "AzureAD"; RequiredVersion = "2.0.2.182"; SubModules = @() }, - @{ ModuleName = "Microsoft.Graph"; RequiredVersion = "2.4.0"; SubModules = @("Groups", "DeviceManagement", "Users", "Identity.DirectoryManagement", "Identity.SignIns") }, + @{ ModuleName = "Microsoft.Graph"; RequiredVersion = "2.4.0"; SubModules = @("DeviceManagement", "Users", "Identity.DirectoryManagement", "Identity.SignIns") }, @{ ModuleName = "Microsoft.Online.SharePoint.PowerShell"; RequiredVersion = "16.0.24009.12000"; SubModules = @() }, @{ ModuleName = "MicrosoftTeams"; RequiredVersion = "5.5.0"; SubModules = @() } ) diff --git a/source/Private/Measure-AuditResult.ps1 b/source/Private/Measure-AuditResult.ps1 index 46c787a..4a8b43b 100644 --- a/source/Private/Measure-AuditResult.ps1 +++ b/source/Private/Measure-AuditResult.ps1 @@ -18,15 +18,15 @@ function Measure-AuditResult { $passPercentage = if ($totalTests -eq 0) { 0 } else { [math]::Round(($passedTests / $totalTests) * 100, 2) } # Display the pass percentage to the user - Write-Host "Audit completed. $passedTests out of $totalTests tests passed." -ForegroundColor Cyan - Write-Host "Your passing percentage is $passPercentage%." -ForegroundColor Magenta + Write-Verbose "Audit completed. $passedTests out of $totalTests tests passed." + Write-Verbose "Your passing percentage is $passPercentage%." # Display details of failed tests if ($FailedTests.Count -gt 0) { - Write-Host "The following tests failed to complete:" -ForegroundColor Red + Write-Verbose "The following tests failed to complete:" foreach ($failedTest in $FailedTests) { - Write-Host "Test: $($failedTest.Test)" -ForegroundColor Yellow - Write-Host "Error: $($failedTest.Error)" -ForegroundColor Yellow + Write-Verbose "Test: $($failedTest.Test)" + Write-Verbose "Error: $($failedTest.Error)" } } } diff --git a/source/Public/Export-M365SecurityAuditTable.ps1 b/source/Public/Export-M365SecurityAuditTable.ps1 index 45eeb00..8110235 100644 --- a/source/Public/Export-M365SecurityAuditTable.ps1 +++ b/source/Public/Export-M365SecurityAuditTable.ps1 @@ -49,7 +49,7 @@ https://criticalsolutionsnetwork.github.io/M365FoundationsCISReport/#Export-M365SecurityAuditTable #> function Export-M365SecurityAuditTable { - [CmdletBinding()] + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true, Position = 1, ParameterSetName = "ExportAllResultsFromAuditResults")] @@ -74,12 +74,21 @@ function Export-M365SecurityAuditTable { [switch]$ExportOriginalTests, [Parameter(Mandatory = $false, ParameterSetName = "ExportAllResultsFromAuditResults")] [Parameter(Mandatory = $false, ParameterSetName = "ExportAllResultsFromCsv")] - [switch]$ExportToExcel + [switch]$ExportToExcel, + # Add Prefix to filename after date when outputting to excel or csv. + [Parameter(Mandatory = $false, ParameterSetName = "ExportAllResultsFromAuditResults")] + [Parameter(Mandatory = $false, ParameterSetName = "ExportAllResultsFromCsv")] + # Validate that the count of letters in the prefix is less than 5. + [ValidateLength(0, 5)] + [string]$Prefix = "Corp" ) Begin { $createdFiles = @() # Initialize an array to keep track of created files + if ($ExportToExcel) { - Assert-ModuleAvailability -ModuleName ImportExcel -RequiredVersion "7.8.9" + if ($PSCmdlet.ShouldProcess("ImportExcel v7.8.9", "Assert-ModuleAvailability")) { + Assert-ModuleAvailability -ModuleName ImportExcel -RequiredVersion "7.8.9" + } } if ($PSCmdlet.ParameterSetName -like "ExportAllResultsFromCsv" -or $PSCmdlet.ParameterSetName -eq "OutputObjectFromCsvSingle") { $AuditResults = Import-Csv -Path $CsvPath | ForEach-Object { @@ -127,94 +136,92 @@ function Export-M365SecurityAuditTable { } End { if ($ExportPath) { - $timestamp = (Get-Date).ToString("yyyy.MM.dd_HH.mm.ss") - $exportedTests = @() - foreach ($result in $results) { - $testDef = $script:TestDefinitionsObject | Where-Object { $_.Rec -eq $result.TestNumber } - if ($testDef) { - $fileName = "$ExportPath\$($timestamp)_$($result.TestNumber).$($testDef.TestFileName -replace '\.ps1$').csv" - if ($result.Details.Count -eq 0) { - Write-Information "No results found for test number $($result.TestNumber)." -InformationAction Continue - } - else { - if (($result.Details -ne "No M365 E3 licenses found.") -and ($result.Details -ne "No M365 E5 licenses found.")) { - if ($ExportToExcel) { - $xlsxPath = [System.IO.Path]::ChangeExtension($fileName, '.xlsx') - $result.Details | Export-Excel -Path $xlsxPath -WorksheetName Table -TableName Table -AutoSize -TableStyle Medium2 - $createdFiles += $xlsxPath # Add the created file to the array + if ($PSCmdlet.ShouldProcess("Export-M365SecurityAuditTable", "Exporting results to $ExportPath")) { + $timestamp = (Get-Date).ToString("yyyy.MM.dd_HH.mm.ss") + $exportedTests = @() + foreach ($result in $results) { + $testDef = $script:TestDefinitionsObject | Where-Object { $_.Rec -eq $result.TestNumber } + if ($testDef) { + $fileName = "$ExportPath\$($timestamp)_$($result.TestNumber).$($testDef.TestFileName -replace '\.ps1$').csv" + if ($result.Details.Count -eq 0) { + Write-Information "No results found for test number $($result.TestNumber)." + } + else { + if (($result.Details -ne "No M365 E3 licenses found.") -and ($result.Details -ne "No M365 E5 licenses found.")) { + if ($ExportToExcel) { + $xlsxPath = [System.IO.Path]::ChangeExtension($fileName, '.xlsx') + $result.Details | Export-Excel -Path $xlsxPath -WorksheetName Table -TableName Table -AutoSize -TableStyle Medium2 + $createdFiles += $xlsxPath # Add the created file to the array + } + else { + $result.Details | Export-Csv -Path $fileName -NoTypeInformation + $createdFiles += $fileName # Add the created file to the array + } + $exportedTests += $result.TestNumber } - else { - $result.Details | Export-Csv -Path $fileName -NoTypeInformation - $createdFiles += $fileName # Add the created file to the array - } - $exportedTests += $result.TestNumber } } } - } - if ($exportedTests.Count -gt 0) { - Write-Information "The following tests were exported: $($exportedTests -join ', ')" -InformationAction Continue - } - else { + if ($exportedTests.Count -gt 0) { + Write-Information "The following tests were exported: $($exportedTests -join ', ')" + } + else { + if ($ExportOriginalTests) { + Write-Information "Full audit results exported however, none of the following tests had exports: `n1.1.1, 1.3.1, 6.1.2, 6.1.3, 7.3.4" + } + else { + Write-Information "No specified tests were included in the export." + } + } if ($ExportOriginalTests) { - Write-Information "Full audit results exported however, none of the following tests had exports: `n1.1.1, 1.3.1, 6.1.2, 6.1.3, 7.3.4" -InformationAction Continue + # Define the test numbers to check + $TestNumbersToCheck = "1.1.1", "1.3.1", "6.1.2", "6.1.3", "7.3.4" + # Check for large details and update the AuditResults array + $updatedAuditResults = Get-ExceededLengthResultDetail -AuditResults $AuditResults -TestNumbersToCheck $TestNumbersToCheck -ExportedTests $exportedTests -DetailsLengthLimit 30000 -PreviewLineCount 25 + $originalFileName = "$ExportPath\$timestamp`_$Prefix-M365FoundationsAudit.csv" + if ($ExportToExcel) { + $xlsxPath = [System.IO.Path]::ChangeExtension($originalFileName, '.xlsx') + $updatedAuditResults | Export-Excel -Path $xlsxPath -WorksheetName Table -TableName Table -AutoSize -TableStyle Medium2 + $createdFiles += $xlsxPath # Add the created file to the array + } + else { + $updatedAuditResults | Export-Csv -Path $originalFileName -NoTypeInformation + $createdFiles += $originalFileName # Add the created file to the array + } } - else { - Write-Information "No specified tests were included in the export." -InformationAction Continue + # Hash each file and add it to a dictionary + # Hash each file and save the hashes to a text file + $hashFilePath = "$ExportPath\$timestamp`_Hashes.txt" + $fileHashes = @() + foreach ($file in $createdFiles) { + $hash = Get-FileHash -Path $file -Algorithm SHA256 + $fileHashes += "$($file): $($hash.Hash)" } - } - if ($ExportOriginalTests) { - # Define the test numbers to check - $TestNumbersToCheck = "1.1.1", "1.3.1", "6.1.2", "6.1.3", "7.3.4" - # Check for large details and update the AuditResults array - $updatedAuditResults = Get-ExceededLengthResultDetail -AuditResults $AuditResults -TestNumbersToCheck $TestNumbersToCheck -ExportedTests $exportedTests -DetailsLengthLimit 30000 -PreviewLineCount 25 - $originalFileName = "$ExportPath\$timestamp`_M365FoundationsAudit.csv" - if ($ExportToExcel) { - $xlsxPath = [System.IO.Path]::ChangeExtension($originalFileName, '.xlsx') - $updatedAuditResults | Export-Excel -Path $xlsxPath -WorksheetName Table -TableName Table -AutoSize -TableStyle Medium2 - $createdFiles += $xlsxPath # Add the created file to the array + $fileHashes | Set-Content -Path $hashFilePath + $createdFiles += $hashFilePath # Add the hash file to the array + # Create a zip file and add all the created files + $zipFilePath = "$ExportPath\$timestamp`_$Prefix-M365FoundationsAudit.zip" + Compress-Archive -Path $createdFiles -DestinationPath $zipFilePath + # Remove the original files after they have been added to the zip + foreach ($file in $createdFiles) { + Remove-Item -Path $file -Force } - else { - $updatedAuditResults | Export-Csv -Path $originalFileName -NoTypeInformation - $createdFiles += $originalFileName # Add the created file to the array + # Compute the hash for the zip file and rename it + $zipHash = Get-FileHash -Path $zipFilePath -Algorithm SHA256 + $newZipFilePath = "$ExportPath\$timestamp`_$Prefix-M365FoundationsAudit_$($zipHash.Hash.Substring(0, 8)).zip" + Rename-Item -Path $zipFilePath -NewName $newZipFilePath + # Output the zip file path with hash + return [PSCustomObject]@{ + ZipFilePath = $newZipFilePath } } - # Hash each file and add it to a dictionary - # Hash each file and save the hashes to a text file - $hashFilePath = "$ExportPath\$timestamp`_Hashes.txt" - $fileHashes = @() - foreach ($file in $createdFiles) { - $hash = Get-FileHash -Path $file -Algorithm SHA256 - $fileHashes += "$($file): $($hash.Hash)" - } - $fileHashes | Set-Content -Path $hashFilePath - $createdFiles += $hashFilePath # Add the hash file to the array - - # Create a zip file and add all the created files - $zipFilePath = "$ExportPath\$timestamp`_M365FoundationsAudit.zip" - Compress-Archive -Path $createdFiles -DestinationPath $zipFilePath - - # Remove the original files after they have been added to the zip - foreach ($file in $createdFiles) { - Remove-Item -Path $file -Force - } - - # Compute the hash for the zip file and rename it - $zipHash = Get-FileHash -Path $zipFilePath -Algorithm SHA256 - $newZipFilePath = "$ExportPath\$timestamp`_M365FoundationsAudit_$($zipHash.Hash.Substring(0, 8)).zip" - Rename-Item -Path $zipFilePath -NewName $newZipFilePath - - # Output the zip file path with hash - [PSCustomObject]@{ - ZipFilePath = $newZipFilePath - } } # End of ExportPath elseif ($OutputTestNumber) { if ($results[0].Details) { return $results[0].Details } else { - Write-Information "No results found for test number $($OutputTestNumber)." -InformationAction Continue + Write-Information "No results found for test number $($OutputTestNumber)." } } else { diff --git a/source/Public/Invoke-M365SecurityAudit.ps1 b/source/Public/Invoke-M365SecurityAudit.ps1 index 5b81453..baeca4e 100644 --- a/source/Public/Invoke-M365SecurityAudit.ps1 +++ b/source/Public/Invoke-M365SecurityAudit.ps1 @@ -139,7 +139,8 @@ https://criticalsolutionsnetwork.github.io/M365FoundationsCISReport/#Invoke-M365SecurityAudit #> function Invoke-M365SecurityAudit { - [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'Default')] + # Add confirm to high + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High" , DefaultParameterSetName = 'Default')] [OutputType([CISAuditResult[]])] param ( [Parameter(Mandatory = $false, HelpMessage = "The SharePoint tenant admin URL, which should end with '-admin.sharepoint.com'. If not specified none of the Sharepoint Online tests will run.")] @@ -206,7 +207,7 @@ function Invoke-M365SecurityAudit { ) Begin { if ($script:MaximumFunctionCount -lt 8192) { - Write-Verbose "Setting the `$script:MaximumFunctionCount to 8192 for the test run." -Verbose + Write-Verbose "Setting the `$script:MaximumFunctionCount to 8192 for the test run." $script:MaximumFunctionCount = 8192 } # Ensure required modules are installed @@ -214,8 +215,8 @@ function Invoke-M365SecurityAudit { # Format the required modules list $requiredModulesFormatted = Format-RequiredModuleList -RequiredModules $requiredModules # Check and install required modules if necessary - if (!($NoModuleCheck) -and $PSCmdlet.ShouldProcess("Check for required modules: $requiredModulesFormatted", "Check")) { - Write-Host "Checking for and installing required modules..." -ForegroundColor DarkMagenta + if (!($NoModuleCheck) -and $PSCmdlet.ShouldProcess("Modules: $requiredModulesFormatted", "Assert-ModuleAvailability")) { + Write-Information "Checking for and installing required modules..." foreach ($module in $requiredModules) { Assert-ModuleAvailability -ModuleName $module.ModuleName -RequiredVersion $module.RequiredVersion -SubModules $module.SubModules } @@ -265,46 +266,59 @@ function Invoke-M365SecurityAudit { try { $actualUniqueConnections = Get-UniqueConnection -Connections $requiredConnections if (!($DoNotConnect) -and $PSCmdlet.ShouldProcess("Establish connections to Microsoft 365 services: $($actualUniqueConnections -join ', ')", "Connect")) { - Write-Host "Establishing connections to Microsoft 365 services: $($actualUniqueConnections -join ', ')" -ForegroundColor DarkMagenta + Write-Information "Establishing connections to Microsoft 365 services: $($actualUniqueConnections -join ', ')" Connect-M365Suite -TenantAdminUrl $TenantAdminUrl -RequiredConnections $requiredConnections -SkipConfirmation:$DoNotConfirmConnections } } catch { - Write-Host "Connection execution aborted: $_" -ForegroundColor Red - break + Throw "Connection execution aborted: $_" } try { - Write-Host "A total of $($totalTests) tests were selected to run..." -ForegroundColor DarkMagenta - # Import the test functions - $testFiles | ForEach-Object { - $currentTestIndex++ - Write-Progress -Activity "Loading Test Scripts" -Status "Loading $($currentTestIndex) of $($totalTests): $($_.Name)" -PercentComplete (($currentTestIndex / $totalTests) * 100) - Try { - # Dot source the test function - . $_.FullName + if ($PSCmdlet.ShouldProcess("Measure and display audit results for $($totalTests) tests", "Measure")) { + Write-Information "A total of $($totalTests) tests were selected to run..." + # Import the test functions + $testFiles | ForEach-Object { + $currentTestIndex++ + Write-Progress -Activity "Loading Test Scripts" -Status "Loading $($currentTestIndex) of $($totalTests): $($_.Name)" -PercentComplete (($currentTestIndex / $totalTests) * 100) + Try { + # Dot source the test function + . $_.FullName + } + Catch { + # Log the error and add the test to the failed tests collection + Write-Verbose "Failed to load test function $($_.Name): $_" + $script:FailedTests.Add([PSCustomObject]@{ Test = $_.Name; Error = $_ }) + } } - Catch { - # Log the error and add the test to the failed tests collection - Write-Verbose "Failed to load test function $($_.Name): $_" -Verbose - $script:FailedTests.Add([PSCustomObject]@{ Test = $_.Name; Error = $_ }) - } - } - $currentTestIndex = 0 - # Execute each test function from the prepared list - foreach ($testFunction in $testFiles) { - $currentTestIndex++ - Write-Progress -Activity "Executing Tests" -Status "Executing $($currentTestIndex) of $($totalTests): $($testFunction.Name)" -PercentComplete (($currentTestIndex / $totalTests) * 100) - $functionName = $testFunction.BaseName - if ($PSCmdlet.ShouldProcess($functionName, "Execute test")) { + $currentTestIndex = 0 + # Execute each test function from the prepared list + foreach ($testFunction in $testFiles) { + $currentTestIndex++ + Write-Progress -Activity "Executing Tests" -Status "Executing $($currentTestIndex) of $($totalTests): $($testFunction.Name)" -PercentComplete (($currentTestIndex / $totalTests) * 100) + $functionName = $testFunction.BaseName + Write-Information "Executing test function: $functionName" $auditResult = Invoke-TestFunction -FunctionFile $testFunction -DomainName $DomainName -ApprovedCloudStorageProviders $ApprovedCloudStorageProviders -ApprovedFederatedDomains $ApprovedFederatedDomains # Add the result to the collection [void]$allAuditResults.Add($auditResult) } + # Call the private function to calculate and display results + Measure-AuditResult -AllAuditResults $allAuditResults -FailedTests $script:FailedTests + # Return all collected audit results + # Define the test numbers to check + $TestNumbersToCheck = "1.1.1", "1.3.1", "6.1.2", "6.1.3", "7.3.4" + # Check for large details in the audit results + $exceedingTests = Get-ExceededLengthResultDetail -AuditResults $allAuditResults -TestNumbersToCheck $TestNumbersToCheck -ReturnExceedingTestsOnly -DetailsLengthLimit 30000 + if ($exceedingTests.Count -gt 0) { + Write-Information "The following tests exceeded the details length limit: $($exceedingTests -join ', ')" + Write-Information "( Assuming the results were instantiated. Ex: `$object = invoke-M365SecurityAudit )`nUse the following command and adjust as necessary to view the full details of the test results:" + Write-Information "Export-M365SecurityAuditTable -ExportAllTests -AuditResults `$object -ExportPath `"C:\temp`" -ExportOriginalTests" + } + return $allAuditResults.ToArray() | Sort-Object -Property Rec } } catch { # Log the error and add the test to the failed tests collection - Write-Verbose "Invoke-M365SecurityAudit: Failed to load test function $($_.Name): $_" -Verbose + throw "Failed to execute test function $($testFunction.Name): $_" $script:FailedTests.Add([PSCustomObject]@{ Test = $_.Name; Error = $_ }) } finally { @@ -315,20 +329,6 @@ function Invoke-M365SecurityAudit { } } End { - if ($PSCmdlet.ShouldProcess("Measure and display audit results for $($totalTests) tests", "Measure")) { - # Call the private function to calculate and display results - Measure-AuditResult -AllAuditResults $allAuditResults -FailedTests $script:FailedTests - # Return all collected audit results - # Define the test numbers to check - $TestNumbersToCheck = "1.1.1", "1.3.1", "6.1.2", "6.1.3", "7.3.4" - # Check for large details in the audit results - $exceedingTests = Get-ExceededLengthResultDetail -AuditResults $allAuditResults -TestNumbersToCheck $TestNumbersToCheck -ReturnExceedingTestsOnly -DetailsLengthLimit 30000 - if ($exceedingTests.Count -gt 0) { - Write-Information "The following tests exceeded the details length limit: $($exceedingTests -join ', ')" -InformationAction Continue - Write-Host "(Assuming the results were instantiated. Ex: `$object = invoke-M365SecurityAudit) Use the following command and adjust as neccesary to view the full details of the test results:" -ForegroundColor DarkCyan - Write-Host "Export-M365SecurityAuditTable -ExportAllTests -AuditResults `$object -ExportPath `"C:\temp`" -ExportOriginalTests" -ForegroundColor Green - } - return $allAuditResults.ToArray() | Sort-Object -Property Rec - } + } } \ No newline at end of file