diff --git a/.gitignore b/.gitignore index d18e461..dfd31be 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ output/ markdownissues.txt node_modules package-lock.json -Aligned.xlsx \ No newline at end of file +Aligned.xlsx +test-gh1.ps1 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b76155..3e23c30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ The format is based on and uses the types of changes according to [Keep a Change ### Added +- Added `Export-M365SecurityAuditTable` public function to export applicable audit results to a table format. +- Added paramter to `Export-M365SecurityAuditTable` to specify output of the original audit results. +- Added `Remove-RowsWithEmptyCSVStatus` public function to remove rows with empty status from the CSV file. +- Added `Get-Action` private function to retrieve the action for the test 6.1.2 and 6.1.3 tests. +- Added output modifications to tests that produce tables to ensure they can be exported with the new `Export-M365SecurityAuditTable` function. + +## [0.1.11] - 2024-06-14 + +### Added + - Added Get-MFAStatus function to help with auditing mfa for conditional access controls. ### Fixed diff --git a/README.md b/README.md index 9906e7a..ac523d4 100644 Binary files a/README.md and b/README.md differ diff --git a/docs/index.html b/docs/index.html index b2bd601..1e46fa7 100644 Binary files a/docs/index.html and b/docs/index.html differ diff --git a/helpers/Build-Help.ps1 b/helpers/Build-Help.ps1 index fe923fc..3a5d89a 100644 --- a/helpers/Build-Help.ps1 +++ b/helpers/Build-Help.ps1 @@ -4,7 +4,7 @@ Import-Module .\output\module\M365FoundationsCISReport\*\*.psd1 <# - $ver = "v0.1.10" + $ver = "v0.1.11" git checkout main git pull origin main git tag -a $ver -m "Release version $ver refactor Update" @@ -13,73 +13,3 @@ Import-Module .\output\module\M365FoundationsCISReport\*\*.psd1 git push origin $ver # git tag -d $ver #> - -# Refresh authentication to ensure the correct scopes -gh auth refresh -s project,read:project,write:project,repo - -# Create the project -gh project create --owner CriticalSolutionsNetwork --title "Test Validation Project" - -$repoOwner = "CriticalSolutionsNetwork" -$repoName = "M365FoundationsCISReport" -$directoryPath = ".\source\tests" -$projectName = "Test Validation Project" - -# Function to create GitHub issues -function Create-GitHubIssue { - param ( - [string]$title, - [string]$body, - [string]$project - ) - - # Create the issue and add it to the specified project - $issue = gh issue create --repo "$repoOwner/$repoName" --title "$title" --body "$body" --project "$project" - return $issue -} - -# Load test definitions from CSV -$testDefinitionsPath = ".\source\helper\TestDefinitions.csv" -$testDefinitions = Import-Csv -Path $testDefinitionsPath - -# Iterate over each .ps1 file in the directory -Get-ChildItem -Path $directoryPath -Filter "*.ps1" | ForEach-Object { - $fileName = $_.Name - $testDefinition = $testDefinitions | Where-Object { $_.TestFileName -eq $fileName } - - if ($testDefinition) { - $rec = $testDefinition.Rec - $elevel = $testDefinition.ELevel - $profileLevel = $testDefinition.ProfileLevel - $ig1 = $testDefinition.IG1 - $ig2 = $testDefinition.IG2 - $ig3 = $testDefinition.IG3 - $connection = $testDefinition.Connection - - $issueTitle = "Rec: $rec - Validate $fileName, ELevel: $elevel, ProfileLevel: $profileLevel, IG1: $ig1, IG2: $ig2, IG3: $ig3, Connection: $connection" - $issueBody = @" -# Validation for $fileName - -## Tasks -- [ ] Validate test for a pass - - Description of passing criteria: -- [ ] Validate test for a fail - - Description of failing criteria: -- [ ] Add notes and observations - - Placeholder for additional notes: -"@ - - # Create the issue using GitHub CLI - try { - Create-GitHubIssue -title "$issueTitle" -body "$issueBody" -project "$projectName" - Write-Output "Created issue for $fileName" - } catch { - Write-Error "Failed to create issue for $fileName : $_" - } - - # Introduce a delay of 2 seconds - Start-Sleep -Seconds 2 - } else { - Write-Warning "No matching test definition found for $fileName" - } -} diff --git a/source/Private/Format-MissingAction.ps1 b/source/Private/Format-MissingAction.ps1 deleted file mode 100644 index 5440486..0000000 --- a/source/Private/Format-MissingAction.ps1 +++ /dev/null @@ -1,29 +0,0 @@ -function Format-MissingAction { - [CmdletBinding()] - [OutputType([hashtable])] - param ( - [array]$missingActions - ) - - $actionGroups = @{ - "Admin" = @() - "Delegate" = @() - "Owner" = @() - } - - foreach ($action in $missingActions) { - if ($action -match "(Admin|Delegate|Owner) action '([^']+)' missing") { - $type = $matches[1] - $actionName = $matches[2] - $actionGroups[$type] += $actionName - } - } - - $formattedResults = @{ - Admin = $actionGroups["Admin"] -join ', ' - Delegate = $actionGroups["Delegate"] -join ', ' - Owner = $actionGroups["Owner"] -join ', ' - } - - return $formattedResults -} diff --git a/source/Private/Get-Action.ps1 b/source/Private/Get-Action.ps1 new file mode 100644 index 0000000..3d24f11 --- /dev/null +++ b/source/Private/Get-Action.ps1 @@ -0,0 +1,113 @@ +function Get-Action { + [CmdletBinding(DefaultParameterSetName = "GetDictionaries")] + param ( + [Parameter(Position = 0, ParameterSetName = "GetDictionaries")] + [switch]$Dictionaries, + + [Parameter(Position = 0, ParameterSetName = "ConvertActions")] + [string[]]$Actions, + + [Parameter(Position = 1, Mandatory = $true, ParameterSetName = "ConvertActions")] + [ValidateSet("Admin", "Delegate", "Owner")] + [string]$ActionType, + + [Parameter(Position = 0, ParameterSetName = "ReverseActions")] + [string[]]$AbbreviatedActions, + + [Parameter(Position = 1, Mandatory = $true, ParameterSetName = "ReverseActions")] + [ValidateSet("Admin", "Delegate", "Owner")] + [string]$ReverseActionType + ) + + $Dictionary = @{ + AdminActions = @{ + ApplyRecord = 'AR' + Copy = 'CP' + Create = 'CR' + FolderBind = 'FB' + HardDelete = 'HD' + MailItemsAccessed = 'MIA' + Move = 'MV' + MoveToDeletedItems = 'MTDI' + SendAs = 'SA' + SendOnBehalf = 'SOB' + Send = 'SD' + SoftDelete = 'SD' + Update = 'UP' + UpdateCalendarDelegation = 'UCD' + UpdateFolderPermissions = 'UFP' + UpdateInboxRules = 'UIR' + } + DelegateActions = @{ + ApplyRecord = 'AR' + Create = 'CR' + FolderBind = 'FB' + HardDelete = 'HD' + MailItemsAccessed = 'MIA' + Move = 'MV' + MoveToDeletedItems = 'MTDI' + SendAs = 'SA' + SendOnBehalf = 'SOB' + SoftDelete = 'SD' + Update = 'UP' + UpdateFolderPermissions = 'UFP' + UpdateInboxRules = 'UIR' + } + OwnerActions = @{ + ApplyRecord = 'AR' + Create = 'CR' + HardDelete = 'HD' + MailboxLogin = 'ML' + MailItemsAccessed = 'MIA' + Move = 'MV' + MoveToDeletedItems = 'MTDI' + Send = 'SD' + SoftDelete = 'SD' + Update = 'UP' + UpdateCalendarDelegation = 'UCD' + UpdateFolderPermissions = 'UFP' + UpdateInboxRules = 'UIR' + } + } + + switch ($PSCmdlet.ParameterSetName) { + "GetDictionaries" { + return $Dictionary + } + "ConvertActions" { + $actionDictionary = switch ($ActionType) { + "Admin" { $Dictionary.AdminActions } + "Delegate" { $Dictionary.DelegateActions } + "Owner" { $Dictionary.OwnerActions } + } + + $abbreviatedActions = @() + foreach ($action in $Actions) { + if ($actionDictionary.ContainsKey($action)) { + $abbreviatedActions += $actionDictionary[$action] + } + } + return $abbreviatedActions + } + "ReverseActions" { + $reverseDictionary = @{} + $originalDictionary = switch ($ReverseActionType) { + "Admin" { $Dictionary.AdminActions } + "Delegate" { $Dictionary.DelegateActions } + "Owner" { $Dictionary.OwnerActions } + } + + foreach ($key in $originalDictionary.Keys) { + $reverseDictionary[$originalDictionary[$key]] = $key + } + + $fullNames = @() + foreach ($abbrAction in $AbbreviatedActions) { + if ($reverseDictionary.ContainsKey($abbrAction)) { + $fullNames += $reverseDictionary[$abbrAction] + } + } + return $fullNames + } + } +} diff --git a/source/Private/Get-ExceededLengthResultDetail.ps1 b/source/Private/Get-ExceededLengthResultDetail.ps1 new file mode 100644 index 0000000..ea2bee5 --- /dev/null +++ b/source/Private/Get-ExceededLengthResultDetail.ps1 @@ -0,0 +1,54 @@ +function Get-ExceededLengthResultDetail { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ParameterSetName = 'UpdateArray')] + [Parameter(Mandatory = $true, ParameterSetName = 'ReturnExceedingTests')] + [object[]]$AuditResults, + + [Parameter(Mandatory = $true, ParameterSetName = 'UpdateArray')] + [Parameter(Mandatory = $true, ParameterSetName = 'ReturnExceedingTests')] + [string[]]$TestNumbersToCheck, + + [Parameter(Mandatory = $true, ParameterSetName = 'UpdateArray')] + [string[]]$ExportedTests, + + [Parameter(Mandatory = $true, ParameterSetName = 'ReturnExceedingTests')] + [switch]$ReturnExceedingTestsOnly, + + [int]$DetailsLengthLimit = 30000, + + [Parameter(Mandatory = $true, ParameterSetName = 'UpdateArray')] + [int]$PreviewLineCount = 50 + ) + + $exceedingTests = @() + $updatedResults = @() + + for ($i = 0; $i -lt $AuditResults.Count; $i++) { + $auditResult = $AuditResults[$i] + if ($auditResult.Rec -in $TestNumbersToCheck) { + if ($auditResult.Details.Length -gt $DetailsLengthLimit) { + if ($ReturnExceedingTestsOnly) { + $exceedingTests += $auditResult.Rec + } else { + $previewLines = ($auditResult.Details -split '\r?\n' | Select-Object -First $PreviewLineCount) -join "`n" + $message = "The test result is too large to be exported to CSV. Use the audit result and the export function for full output.`n`nPreview:`n$previewLines" + + if ($ExportedTests -contains $auditResult.Rec) { + Write-Information "The test result for $($auditResult.Rec) is too large for CSV and was included in the export. Check the exported files." + $auditResult.Details = $message + } else { + $auditResult.Details = $message + } + } + } + } + $updatedResults += $auditResult + } + + if ($ReturnExceedingTestsOnly) { + return $exceedingTests + } else { + return $updatedResults + } +} \ No newline at end of file diff --git a/source/Private/Initialize-LargeTestTable.ps1 b/source/Private/Initialize-LargeTestTable.ps1 new file mode 100644 index 0000000..f5e2d52 --- /dev/null +++ b/source/Private/Initialize-LargeTestTable.ps1 @@ -0,0 +1,36 @@ +<# + .SYNOPSIS + This function generates a large table with the specified number of lines. + .DESCRIPTION + This function generates a large table with the specified number of lines. The table has a header and each line has the same format. + .EXAMPLE + Initialize-LargeTestTable -lineCount 1000 + .PARAMETER lineCount + The number of lines to generate. + .INPUTS + System.Int32 + .OUTPUTS + System.String + .NOTES + The function is intended for testing purposes. +#> +function Initialize-LargeTestTable { + [cmdletBinding()] + [OutputType([string])] + param( + [Parameter()] + [int]$lineCount = 1000 # Number of lines to generate + ) + process { + $header = "UserPrincipalName|AuditEnabled|AdminActionsMissing|DelegateActionsMissing|OwnerActionsMissing" + $lineTemplate = "user{0}@contosonorthwind.net|True|FB,CP,MV|FB,MV|ML,MV,CR" + # Generate the header and lines + $lines = @($header) + for ($i = 1; $i -le $lineCount; $i++) { + $lines += [string]::Format($lineTemplate, $i) + } + $output = $lines -join "`n" + Write-Host "Details character count: $($output.Length)" + return $output + } +} \ No newline at end of file diff --git a/source/Public/Export-M365SecurityAuditTable.ps1 b/source/Public/Export-M365SecurityAuditTable.ps1 new file mode 100644 index 0000000..bc15887 --- /dev/null +++ b/source/Public/Export-M365SecurityAuditTable.ps1 @@ -0,0 +1,196 @@ +<# + .SYNOPSIS + Exports M365 security audit results to a CSV file or outputs a specific test result as an object. + .DESCRIPTION + This function exports M365 security audit results from either an array of CISAuditResult objects or a CSV file. + It can export all results to a specified path or output a specific test result as an object. + .PARAMETER AuditResults + An array of CISAuditResult objects containing the audit results. + .PARAMETER CsvPath + The path to a CSV file containing the audit results. + .PARAMETER OutputTestNumber + The test number to output as an object. Valid values are "1.1.1", "1.3.1", "6.1.2", "6.1.3", "7.3.4". + .PARAMETER ExportAllTests + Switch to export all test results. + .PARAMETER ExportPath + The path where the CSV files will be exported. + .PARAMETER ExportOriginalTests + Switch to export the original audit results to a CSV file. + .INPUTS + [CISAuditResult[]], [string] + .OUTPUTS + [PSCustomObject] + .EXAMPLE + # Output object for a single test number from audit results + Export-M365SecurityAuditTable -AuditResults $object -OutputTestNumber 6.1.2 + .EXAMPLE + # Export all results from audit results to the specified path + Export-M365SecurityAuditTable -ExportAllTests -AuditResults $object -ExportPath "C:\temp" + .EXAMPLE + # Output object for a single test number from CSV + Export-M365SecurityAuditTable -CsvPath "C:\temp\auditresultstoday1.csv" -OutputTestNumber 6.1.2 + .EXAMPLE + # Export all results from CSV to the specified path + Export-M365SecurityAuditTable -ExportAllTests -CsvPath "C:\temp\auditresultstoday1.csv" -ExportPath "C:\temp" + .EXAMPLE + # Export all results from audit results to the specified path along with the original tests + Export-M365SecurityAuditTable -ExportAllTests -AuditResults $object -ExportPath "C:\temp" -ExportOriginalTests + .EXAMPLE + # Export all results from CSV to the specified path along with the original tests + Export-M365SecurityAuditTable -ExportAllTests -CsvPath "C:\temp\auditresultstoday1.csv" -ExportPath "C:\temp" -ExportOriginalTests + .LINK + https://criticalsolutionsnetwork.github.io/M365FoundationsCISReport/#Export-M365SecurityAuditTable +#> +function Export-M365SecurityAuditTable { + [CmdletBinding()] + [OutputType([PSCustomObject])] + param ( + [Parameter(Mandatory = $true, Position = 1, ParameterSetName = "ExportAllResultsFromAuditResults")] + [Parameter(Mandatory = $true, Position = 2, ParameterSetName = "OutputObjectFromAuditResultsSingle")] + [CISAuditResult[]]$AuditResults, + + [Parameter(Mandatory = $true, Position = 1, ParameterSetName = "ExportAllResultsFromCsv")] + [Parameter(Mandatory = $true, Position = 2, ParameterSetName = "OutputObjectFromCsvSingle")] + [ValidateScript({ (Test-Path $_) -and ((Get-Item $_).PSIsContainer -eq $false) })] + [string]$CsvPath, + + [Parameter(Mandatory = $true, Position = 1, ParameterSetName = "OutputObjectFromAuditResultsSingle")] + [Parameter(Mandatory = $true, Position = 1, ParameterSetName = "OutputObjectFromCsvSingle")] + [ValidateSet("1.1.1", "1.3.1", "6.1.2", "6.1.3", "7.3.4")] + [string]$OutputTestNumber, + + [Parameter(Mandatory = $true, Position = 0, ParameterSetName = "ExportAllResultsFromAuditResults")] + [Parameter(Mandatory = $true, Position = 0, ParameterSetName = "ExportAllResultsFromCsv")] + [switch]$ExportAllTests, + + [Parameter(Mandatory = $true, ParameterSetName = "ExportAllResultsFromAuditResults")] + [Parameter(Mandatory = $true, ParameterSetName = "ExportAllResultsFromCsv")] + [string]$ExportPath, + + [Parameter(Mandatory = $false, ParameterSetName = "ExportAllResultsFromAuditResults")] + [Parameter(Mandatory = $false, ParameterSetName = "ExportAllResultsFromCsv")] + [switch]$ExportOriginalTests + ) + + if ($PSCmdlet.ParameterSetName -like "ExportAllResultsFromCsv" -or $PSCmdlet.ParameterSetName -eq "OutputObjectFromCsvSingle") { + $AuditResults = Import-Csv -Path $CsvPath | ForEach-Object { + $params = @{ + Rec = $_.Rec + Result = [bool]$_.Result + Status = $_.Status + Details = $_.Details + FailureReason = $_.FailureReason + } + Initialize-CISAuditResult @params + } + } + + if ($ExportAllTests) { + $TestNumbers = "1.1.1", "1.3.1", "6.1.2", "6.1.3", "7.3.4" + } + + $results = @() + + $testsToProcess = if ($OutputTestNumber) { @($OutputTestNumber) } else { $TestNumbers } + + foreach ($test in $testsToProcess) { + $auditResult = $AuditResults | Where-Object { $_.Rec -eq $test } + if (-not $auditResult) { + Write-Information "No audit results found for the test number $test." + continue + } + + switch ($test) { + "6.1.2" { + $details = $auditResult.Details + $csv = $details | ConvertFrom-Csv -Delimiter '|' + + if ($null -ne $csv) { + foreach ($row in $csv) { + $row.AdminActionsMissing = (Get-Action -AbbreviatedActions $row.AdminActionsMissing.Split(',') -ReverseActionType Admin | Where-Object { $_ -notin @("MailItemsAccessed", "Send") }) -join ',' + $row.DelegateActionsMissing = (Get-Action -AbbreviatedActions $row.DelegateActionsMissing.Split(',') -ReverseActionType Delegate | Where-Object { $_ -notin @("MailItemsAccessed") }) -join ',' + $row.OwnerActionsMissing = (Get-Action -AbbreviatedActions $row.OwnerActionsMissing.Split(',') -ReverseActionType Owner | Where-Object { $_ -notin @("MailItemsAccessed", "Send") }) -join ',' + } + $newObjectDetails = $csv + } + else { + $newObjectDetails = $details + } + $results += [PSCustomObject]@{ TestNumber = $test; Details = $newObjectDetails } + } + "6.1.3" { + $details = $auditResult.Details + $csv = $details | ConvertFrom-Csv -Delimiter '|' + + if ($null -ne $csv) { + foreach ($row in $csv) { + $row.AdminActionsMissing = (Get-Action -AbbreviatedActions $row.AdminActionsMissing.Split(',') -ReverseActionType Admin) -join ',' + $row.DelegateActionsMissing = (Get-Action -AbbreviatedActions $row.DelegateActionsMissing.Split(',') -ReverseActionType Delegate) -join ',' + $row.OwnerActionsMissing = (Get-Action -AbbreviatedActions $row.OwnerActionsMissing.Split(',') -ReverseActionType Owner) -join ',' + } + $newObjectDetails = $csv + } + else { + $newObjectDetails = $details + } + $results += [PSCustomObject]@{ TestNumber = $test; Details = $newObjectDetails } + } + Default { + $details = $auditResult.Details + $csv = $details | ConvertFrom-Csv -Delimiter '|' + $results += [PSCustomObject]@{ TestNumber = $test; Details = $csv } + } + } + } + + 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 { + $result.Details | Export-Csv -Path $fileName -NoTypeInformation + $exportedTests += $result.TestNumber + } + } + } + if ($exportedTests.Count -gt 0) { + Write-Information "The following tests were exported: $($exportedTests -join ', ')" -InformationAction Continue + } + else { + if ($ExportOriginalTests) { + Write-Information "No specified tests were included in the export other than the full audit results." -InformationAction Continue + } + else { + Write-Information "No specified tests were included in the export." -InformationAction Continue + } + } + + 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" + $updatedAuditResults | Export-Csv -Path $originalFileName -NoTypeInformation + } + } + elseif ($OutputTestNumber) { + if ($results[0].Details) { + return $results[0].Details + } + else { + Write-Information "No results found for test number $($OutputTestNumber)." -InformationAction Continue + } + } + else { + Write-Error "No valid operation specified. Please provide valid parameters." + } +} diff --git a/source/Public/Get-AdminRoleUserLicense.ps1 b/source/Public/Get-AdminRoleUserLicense.ps1 index 9cd86c3..1af177a 100644 --- a/source/Public/Get-AdminRoleUserLicense.ps1 +++ b/source/Public/Get-AdminRoleUserLicense.ps1 @@ -25,7 +25,6 @@ https://criticalsolutionsnetwork.github.io/M365FoundationsCISReport/#Get-AdminRoleUserLicense #> function Get-AdminRoleUserLicense { - # Set output type to System.Collections.ArrayList [OutputType([System.Collections.ArrayList])] [CmdletBinding()] param ( @@ -42,33 +41,37 @@ function Get-AdminRoleUserLicense { $userIds = [System.Collections.ArrayList]::new() } - Process { - $adminroles = Get-MgRoleManagementDirectoryRoleDefinition | Where-Object { $_.DisplayName -like "*Admin*" } + process { + Write-Verbose "Retrieving all admin roles" + $adminRoleNames = (Get-MgDirectoryRole | Where-Object { $null -ne $_.RoleTemplateId }).DisplayName - foreach ($role in $adminroles) { - $usersInRole = Get-MgRoleManagementDirectoryRoleAssignment -Filter "roleDefinitionId eq '$($role.Id)'" + Write-Verbose "Filtering admin roles" + $adminRoles = Get-MgRoleManagementDirectoryRoleDefinition | Where-Object { ($adminRoleNames -contains $_.DisplayName) -and ($_.DisplayName -ne "Directory Synchronization Accounts") } - foreach ($user in $usersInRole) { - $userDetails = Get-MgUser -UserId $user.PrincipalId -Property "DisplayName, UserPrincipalName, Id, onPremisesSyncEnabled" -ErrorAction SilentlyContinue + 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) { - [void]($userIds.Add($user.PrincipalId)) - [void]( - $adminRoleUsers.Add( - [PSCustomObject]@{ - RoleName = $role.DisplayName - UserName = $userDetails.DisplayName - UserPrincipalName = $userDetails.UserPrincipalName - UserId = $userDetails.Id - HybridUser = $userDetails.onPremisesSyncEnabled - Licenses = $null # Initialize as $null - } - ) - ) + Write-Verbose "Retrieved user details for: $($userDetails.UserPrincipalName)" + [void]($userIds.Add($userDetails.Id)) + [void]($adminRoleUsers.Add([PSCustomObject]@{ + RoleName = $role.DisplayName + UserName = $userDetails.DisplayName + UserPrincipalName = $userDetails.UserPrincipalName + UserId = $userDetails.Id + HybridUser = [bool]$userDetails.OnPremisesSyncEnabled + Licenses = $null # Initialize as $null + })) } } } + Write-Verbose "Retrieving licenses for admin role users" foreach ($userId in $userIds.ToArray() | Select-Object -Unique) { $licenses = Get-MgUserLicenseDetail -UserId $userId -ErrorAction SilentlyContinue if ($licenses) { @@ -80,7 +83,7 @@ function Get-AdminRoleUserLicense { } } - End { + end { Write-Host "Disconnecting from Microsoft Graph..." -ForegroundColor Green Disconnect-MgGraph | Out-Null return $adminRoleUsers diff --git a/source/Public/Invoke-M365SecurityAudit.ps1 b/source/Public/Invoke-M365SecurityAudit.ps1 index 5e94add..cfc5aed 100644 --- a/source/Public/Invoke-M365SecurityAudit.ps1 +++ b/source/Public/Invoke-M365SecurityAudit.ps1 @@ -286,6 +286,16 @@ function Invoke-M365SecurityAudit { # 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 } } diff --git a/source/Public/Remove-RowsWithEmptyCSVStatus.ps1 b/source/Public/Remove-RowsWithEmptyCSVStatus.ps1 new file mode 100644 index 0000000..7258500 --- /dev/null +++ b/source/Public/Remove-RowsWithEmptyCSVStatus.ps1 @@ -0,0 +1,34 @@ +function Remove-RowsWithEmptyCSVStatus { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$FilePath, + + [Parameter(Mandatory = $true)] + [string]$WorksheetName + ) + + # Import the Excel file + $ExcelData = Import-Excel -Path $FilePath -WorksheetName $WorksheetName + + # Check if CSV_Status column exists + if (-not $ExcelData.PSObject.Properties.Match("CSV_Status")) { + throw "CSV_Status column not found in the worksheet." + } + + # Filter rows where CSV_Status is not empty + $FilteredData = $ExcelData | Where-Object { $null -ne $_.CSV_Status -and $_.CSV_Status -ne '' } + + # Get the original file name and directory + $OriginalFileName = [System.IO.Path]::GetFileNameWithoutExtension($FilePath) + $Directory = [System.IO.Path]::GetDirectoryName($FilePath) + + # Create a new file name for the filtered data + $NewFileName = "$OriginalFileName-Filtered.xlsx" + $NewFilePath = Join-Path -Path $Directory -ChildPath $NewFileName + + # Export the filtered data to a new Excel file + $FilteredData | Export-Excel -Path $NewFilePath -WorksheetName $WorksheetName -Show + + Write-Output "Filtered Excel file created at $NewFilePath" +} \ No newline at end of file diff --git a/source/tests/Test-AdministrativeAccountCompliance.ps1 b/source/tests/Test-AdministrativeAccountCompliance.ps1 index 2a1b0a8..e4644ad 100644 --- a/source/tests/Test-AdministrativeAccountCompliance.ps1 +++ b/source/tests/Test-AdministrativeAccountCompliance.ps1 @@ -20,7 +20,12 @@ function Test-AdministrativeAccountCompliance { try { # Retrieve all admin roles Write-Verbose "Retrieving all admin roles" - $adminRoles = Get-MgRoleManagementDirectoryRoleDefinition | Where-Object { $_.DisplayName -like "*Admin*" } + # 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")} + $adminRoleUsers = @() # Loop through each admin role to get role assignments and user details @@ -47,9 +52,10 @@ function Test-AdministrativeAccountCompliance { # 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: $($userDetails.UserPrincipalName), Cloud-Only: $cloudOnlyStatus, Valid Licenses: $validLicensesStatus, Other Applications Assigned: $applicationAssignmentStatus" + Write-Verbose "User: $($userDetails.UserPrincipalName), Cloud-Only: $cloudOnlyStatus, Valid Licenses: $validLicensesStatus, Invalid Licenses: $($invalidLicenses -join ', ')" # Collect user information $adminRoleUsers += [PSCustomObject]@{ @@ -95,13 +101,14 @@ function Test-AdministrativeAccountCompliance { $failureReasons = $failureReasons -join "`n" $failureReason = if ($nonCompliantUsers) { "Non-Compliant Accounts: $($nonCompliantUsers.Count)" - } else { + } + else { "Compliant Accounts: $($uniqueAdminRoleUsers.Count)" } $result = $nonCompliantUsers.Count -eq 0 $status = if ($result) { 'Pass' } else { 'Fail' } - $details = if ($nonCompliantUsers) { "Non-compliant accounts: `nUsername | Roles | Cloud-Only Status | Entra ID License Status | Other Applications Assigned Status`n$failureReasons" } else { "N/A" } + $details = if ($nonCompliantUsers) { "Username | Roles | Cloud-Only Status | EntraID P1/P2 License Status | Other Applications Assigned Status`n$failureReasons" } else { "N/A" } Write-Verbose "Assessment completed. Result: $status" diff --git a/source/tests/Test-BlockMailForwarding.ps1 b/source/tests/Test-BlockMailForwarding.ps1 index f0633ad..4dab7a9 100644 --- a/source/tests/Test-BlockMailForwarding.ps1 +++ b/source/tests/Test-BlockMailForwarding.ps1 @@ -64,7 +64,7 @@ function Test-BlockMailForwarding { if ($nonCompliantSpamPoliciesArray.Count -gt 0) { # Fail Condition B $failureReasons += "Outbound spam policies allowing automatic forwarding found." - $details += "Outbound Spam Policies Details:`nPolicy|AutoForwardingMode" + $details += "Policy|AutoForwardingMode" $details += $nonCompliantSpamPoliciesArray | ForEach-Object { "$($_.Name)|$($_.AutoForwardingMode)" } diff --git a/source/tests/Test-MailboxAuditingE3.ps1 b/source/tests/Test-MailboxAuditingE3.ps1 index d7c699a..4f30450 100644 --- a/source/tests/Test-MailboxAuditingE3.ps1 +++ b/source/tests/Test-MailboxAuditingE3.ps1 @@ -30,20 +30,21 @@ function Test-MailboxAuditingE3 { #. .\source\Classes\CISAuditResult.ps1 $e3SkuPartNumber = "SPE_E3" - $AdminActions = @("ApplyRecord", "Copy", "Create", "FolderBind", "HardDelete", "Move", "MoveToDeletedItems", "SendAs", "SendOnBehalf", "SoftDelete", "Update", "UpdateCalendarDelegation", "UpdateFolderPermissions", "UpdateInboxRules") - $DelegateActions = @("ApplyRecord", "Create", "FolderBind", "HardDelete", "Move", "MoveToDeletedItems", "SendAs", "SendOnBehalf", "SoftDelete", "Update", "UpdateFolderPermissions", "UpdateInboxRules") - $OwnerActions = @("ApplyRecord", "Create", "HardDelete", "MailboxLogin", "Move", "MoveToDeletedItems", "SoftDelete", "Update", "UpdateCalendarDelegation", "UpdateFolderPermissions", "UpdateInboxRules") + + $actionDictionaries = Get-Action -Dictionaries + # E3 specific actions + $AdminActions = $actionDictionaries.AdminActions.Keys | Where-Object { $_ -notin @("MailItemsAccessed", "Send") } + $DelegateActions = $actionDictionaries.DelegateActions.Keys | Where-Object { $_ -notin @("MailItemsAccessed") } + $OwnerActions = $actionDictionaries.OwnerActions.Keys | Where-Object { $_ -notin @("MailItemsAccessed", "Send") } $allFailures = @() - #$allUsers = Get-AzureADUser -All $true - $founde3Sku = Get-MgSubscribedSku -All | Where-Object {$_.SkuPartNumber -eq $e3SkuPartNumber} + $founde3Sku = Get-MgSubscribedSku -All | Where-Object { $_.SkuPartNumber -eq $e3SkuPartNumber } $processedUsers = @{} # Dictionary to track processed users $recnum = "6.1.2" } - process { - if (($founde3Sku.count)-ne 0) { + if ($founde3Sku.Count -ne 0) { $allUsers = Get-MgUser -Filter "assignedLicenses/any(x:x/skuId eq $($founde3Sku.SkuId) )" -All $mailboxes = Get-EXOMailbox -PropertySets Audit try { @@ -53,36 +54,36 @@ function Test-MailboxAuditingE3 { continue } - #$licenseDetails = Get-MgUserLicenseDetail -UserId $user.UserPrincipalName - #$hasOfficeE3 = ($licenseDetails | Where-Object { $_.SkuPartNumber -in $e3SkuPartNumbers }).Count -gt 0 - #Write-Verbose "Evaluating user $($user.UserPrincipalName) for Office E3 license." - $userUPN = $user.UserPrincipalName $mailbox = $mailboxes | Where-Object { $_.UserPrincipalName -eq $user.UserPrincipalName } - $missingActions = @() + $missingAdminActions = @() + $missingDelegateActions = @() + $missingOwnerActions = @() + if ($mailbox.AuditEnabled) { foreach ($action in $AdminActions) { - # Condition B: Checking if the `AuditAdmin` actions include required actions - if ($mailbox.AuditAdmin -notcontains $action) { $missingActions += "Admin action '$action' missing" } + if ($mailbox.AuditAdmin -notcontains $action) { + $missingAdminActions += (Get-Action -Actions $action -ActionType "Admin") + } } foreach ($action in $DelegateActions) { - # Condition C: Checking if the `AuditDelegate` actions include required actions - if ($mailbox.AuditDelegate -notcontains $action) { $missingActions += "Delegate action '$action' missing" } + if ($mailbox.AuditDelegate -notcontains $action) { + $missingDelegateActions += (Get-Action -Actions $action -ActionType "Delegate") + } } foreach ($action in $OwnerActions) { - # Condition D: Checking if the `AuditOwner` actions include required actions - if ($mailbox.AuditOwner -notcontains $action) { $missingActions += "Owner action '$action' missing" } + if ($mailbox.AuditOwner -notcontains $action) { + $missingOwnerActions += (Get-Action -Actions $action -ActionType "Owner") + } } - if ($missingActions.Count -gt 0) { - $formattedActions = Format-MissingAction -missingActions $missingActions - $allFailures += "$userUPN|True|$($formattedActions.Admin)|$($formattedActions.Delegate)|$($formattedActions.Owner)" + if ($missingAdminActions.Count -gt 0 -or $missingDelegateActions.Count -gt 0 -or $missingOwnerActions.Count -gt 0) { + $allFailures += "$userUPN|True|$($missingAdminActions -join ',')|$($missingDelegateActions -join ',')|$($missingOwnerActions -join ',')" } } else { - # Condition A: Checking if mailbox audit logging is enabled - $allFailures += "$userUPN|False|||" + $allFailures += "$userUPN|False|||" # Condition A for fail } # Mark the user as processed @@ -90,7 +91,12 @@ function Test-MailboxAuditingE3 { } # Prepare failure reasons and details based on compliance - $failureReasons = if ($allFailures.Count -eq 0) { "N/A" } else { "Audit issues detected." } + if ($allFailures.Count -eq 0) { + $failureReasons = "N/A" + } + else { + $failureReasons = "Audit issues detected." + } $details = if ($allFailures.Count -eq 0) { "All Office E3 users have correct mailbox audit settings." } @@ -134,14 +140,13 @@ function Test-MailboxAuditingE3 { } end { - #$verbosePreference = 'Continue' $detailsLength = $details.Length Write-Verbose "Character count of the details: $detailsLength" if ($detailsLength -gt 32767) { Write-Verbose "Warning: The character count exceeds the limit for Excel cells." } - #$verbosePreference = 'SilentlyContinue' + return $auditResult } } diff --git a/source/tests/Test-MailboxAuditingE5.ps1 b/source/tests/Test-MailboxAuditingE5.ps1 index a332baf..adca387 100644 --- a/source/tests/Test-MailboxAuditingE5.ps1 +++ b/source/tests/Test-MailboxAuditingE5.ps1 @@ -28,14 +28,15 @@ function Test-MailboxAuditingE5 { # - Condition D: AuditOwner actions do not include all of the following: ApplyRecord, HardDelete, MailItemsAccessed, MoveToDeletedItems, Send, SoftDelete, Update, UpdateCalendarDelegation, UpdateFolderPermissions, UpdateInboxRules. $e5SkuPartNumber = "SPE_E5" - $AdminActions = @("ApplyRecord", "Copy", "Create", "FolderBind", "HardDelete", "MailItemsAccessed", "Move", "MoveToDeletedItems", "SendAs", "SendOnBehalf", "Send", "SoftDelete", "Update", "UpdateCalendarDelegation", "UpdateFolderPermissions", "UpdateInboxRules") - $DelegateActions = @("ApplyRecord", "Create", "FolderBind", "HardDelete", "MailItemsAccessed", "Move", "MoveToDeletedItems", "SendAs", "SendOnBehalf", "SoftDelete", "Update", "UpdateFolderPermissions", "UpdateInboxRules") - $OwnerActions = @("ApplyRecord", "Create", "HardDelete", "MailboxLogin", "Move", "MailItemsAccessed", "MoveToDeletedItems", "Send", "SoftDelete", "Update", "UpdateCalendarDelegation", "UpdateFolderPermissions", "UpdateInboxRules") + $founde5Sku = Get-MgSubscribedSku -All | Where-Object { $_.SkuPartNumber -eq $e5SkuPartNumber } + + $actionDictionaries = Get-Action -Dictionaries + $AdminActions = $actionDictionaries.AdminActions.Keys + $DelegateActions = $actionDictionaries.DelegateActions.Keys + $OwnerActions = $actionDictionaries.OwnerActions.Keys $allFailures = @() - #$allUsers = Get-AzureADUser -All $true - $founde5Sku = Get-MgSubscribedSku -All | Where-Object { $_.SkuPartNumber -eq $e5SkuPartNumber } - $processedUsers = @{} # Dictionary to track processed users + $processedUsers = @{} $recnum = "6.1.3" } @@ -50,35 +51,39 @@ function Test-MailboxAuditingE5 { continue } - #$licenseDetails = Get-MgUserLicenseDetail -UserId $user.UserPrincipalName - #$hasOfficeE5 = ($licenseDetails | Where-Object { $_.SkuPartNumber -in $e5SkuPartNumbers }).Count -gt 0 - #Write-Verbose "Evaluating user $($user.UserPrincipalName) for Office E5 license." $mailbox = $mailboxes | Where-Object { $_.UserPrincipalName -eq $user.UserPrincipalName } $userUPN = $user.UserPrincipalName - #$mailbox = Get-EXOMailbox -Identity $userUPN -PropertySets Audit - $missingActions = @() + $missingAdminActions = @() + $missingDelegateActions = @() + $missingOwnerActions = @() + if ($mailbox.AuditEnabled) { # Validate Admin actions foreach ($action in $AdminActions) { - if ($mailbox.AuditAdmin -notcontains $action) { $missingActions += "Admin action '$action' missing" } # Condition B + if ($mailbox.AuditAdmin -notcontains $action) { + $missingAdminActions += (Get-Action -Actions $action -ActionType "Admin") # Condition B + } } # Validate Delegate actions foreach ($action in $DelegateActions) { - if ($mailbox.AuditDelegate -notcontains $action) { $missingActions += "Delegate action '$action' missing" } # Condition C + if ($mailbox.AuditDelegate -notcontains $action) { + $missingDelegateActions += (Get-Action -Actions $action -ActionType "Delegate") # Condition C + } } # Validate Owner actions foreach ($action in $OwnerActions) { - if ($mailbox.AuditOwner -notcontains $action) { $missingActions += "Owner action '$action' missing" } # Condition D + if ($mailbox.AuditOwner -notcontains $action) { + $missingOwnerActions += (Get-Action -Actions $action -ActionType "Owner") # Condition D + } } - if ($missingActions.Count -gt 0) { - $formattedActions = Format-MissingAction -missingActions $missingActions - $allFailures += "$userUPN|True|$($formattedActions.Admin)|$($formattedActions.Delegate)|$($formattedActions.Owner)" + if ($missingAdminActions.Count -gt 0 -or $missingDelegateActions.Count -gt 0 -or $missingOwnerActions.Count -gt 0) { + $allFailures += "$userUPN|True|$($missingAdminActions -join ',')|$($missingDelegateActions -join ',')|$($missingOwnerActions -join ',')" } } else { - $allFailures += "$userUPN|False|||" + $allFailures += "$userUPN|False|||" # Condition A for fail } # Mark the user as processed @@ -86,14 +91,19 @@ function Test-MailboxAuditingE5 { } # Prepare failure reasons and details based on compliance - $failureReasons = if ($allFailures.Count -eq 0) { "N/A" } else { "Audit issues detected." } + if ($allFailures.Count -eq 0) { + $failureReasons = "N/A" + } + else { + $failureReasons = "Audit issues detected." + } $details = if ($allFailures.Count -eq 0) { "All Office E5 users have correct mailbox audit settings." # Condition A for pass } else { "UserPrincipalName|AuditEnabled|AdminActionsMissing|DelegateActionsMissing|OwnerActionsMissing`n" + ($allFailures -join "`n") # Condition A for fail } - + # $details = Initialize-LargeTestTable -lineCount 3000 # Adjust the lineCount to exceed 32,000 characters # Populate the audit result $params = @{ Rec = $recnum @@ -130,14 +140,13 @@ function Test-MailboxAuditingE5 { } end { - #$verbosePreference = 'Continue' $detailsLength = $details.Length Write-Verbose "Character count of the details: $detailsLength" if ($detailsLength -gt 32767) { Write-Verbose "Warning: The character count exceeds the limit for Excel cells." } - #$verbosePreference = 'SilentlyContinue' + return $auditResult } -} +} \ No newline at end of file diff --git a/source/tests/Test-SafeAttachmentsPolicy.ps1 b/source/tests/Test-SafeAttachmentsPolicy.ps1 index b3cb316..be2faf4 100644 --- a/source/tests/Test-SafeAttachmentsPolicy.ps1 +++ b/source/tests/Test-SafeAttachmentsPolicy.ps1 @@ -4,10 +4,6 @@ function Test-SafeAttachmentsPolicy { param () begin { - # Dot source the class script if necessary - #. .\source\Classes\CISAuditResult.ps1 - - # Initialization code, if needed $recnum = "2.1.4" <# @@ -61,18 +57,20 @@ function Test-SafeAttachmentsPolicy { # Add policy details to the details array $details += [PSCustomObject]@{ - Policy = $policy.Name - Enabled = $policy.Enable - Action = $policy.Action - Failed = $failed + Policy = $policy.Name + Enabled = $policy.Enable + Action = $policy.Action + Failed = $failed } } # The result is a pass if there are no failure reasons $result = $failureReasons.Count -eq 0 - # Format details for output - $detailsString = $details | Format-Table -AutoSize | Out-String + # Format details for output manually + $detailsString = "Policy|Enabled|Action|Failed`n" + ($details | + ForEach-Object {"$($_.Policy)|$($_.Enabled)|$($_.Action)|$($_.Failed)`n"} + ) $failureReasonsString = ($failureReasons | ForEach-Object { $_ }) -join ' ' # Create and populate the CISAuditResult object @@ -103,8 +101,8 @@ function Test-SafeAttachmentsPolicy { Rec = $recnum Result = $false Status = "Fail" - Details = "No M365 E5 licenses found." - FailureReason = "The audit is for M365 E5 licenses and the required EXO commands will not be available otherwise." + Details = "No Safe Attachments policies found." + FailureReason = "The audit needs Safe Attachment features available or required EXO commands will not be available otherwise." } $auditResult = Initialize-CISAuditResult @params } diff --git a/test-gh.ps1 b/test-gh.ps1 deleted file mode 100644 index cb3a8db..0000000 --- a/test-gh.ps1 +++ /dev/null @@ -1,405 +0,0 @@ -$repoOwner = "CriticalSolutionsNetwork" -$repoName = "M365FoundationsCISReport" -$directoryPath = ".\source\tests" -$projectName = "Test Validation Project" - -# Function to create GitHub issues -function Create-GitHubIssue { - param ( - [string]$title, - [string]$body, - [string]$project - ) - - # Create the issue and add it to the specified project - $issue = gh issue create --repo "$repoOwner/$repoName" --title "$title" --body "$body" --project "$project" - return $issue -} - -# Load test definitions from CSV -$testDefinitionsPath = ".\source\helper\TestDefinitions.csv" -$testDefinitions = Import-Csv -Path $testDefinitionsPath - -# Iterate over each .ps1 file in the directory -Get-ChildItem -Path $directoryPath -Filter "*.ps1" | ForEach-Object { - $fileName = $_.Name - $testDefinition = $testDefinitions | Where-Object { $_.TestFileName -eq $fileName } - - if ($testDefinition) { - $rec = $testDefinition.Rec - $elevel = $testDefinition.ELevel - $profileLevel = $testDefinition.ProfileLevel - $ig1 = $testDefinition.IG1 - $ig2 = $testDefinition.IG2 - $ig3 = $testDefinition.IG3 - $connection = $testDefinition.Connection - - $issueTitle = "Rec: $rec - Validate $fileName, ELevel: $elevel, ProfileLevel: $profileLevel, IG1: $ig1, IG2: $ig2, IG3: $ig3, Connection: $connection" - $issueBody = @" -# Validation for $fileName - -## Tasks -- [ ] Validate test for a pass - - Description of passing criteria: -- [ ] Validate test for a fail - - Description of failing criteria: -- [ ] Add notes and observations - - Placeholder for additional notes: -"@ - - # Create the issue using GitHub CLI - try { - Create-GitHubIssue -title "$issueTitle" -body "$issueBody" -project "$projectName" - Write-Output "Created issue for $fileName" - } - catch { - Write-Error "Failed to create issue for $fileName`: $_" - } - - # Introduce a delay of 2 seconds - Start-Sleep -Seconds 2 - } - else { - Write-Warning "No matching test definition found for $fileName" - } -} -###################################### -$repoOwner = "CriticalSolutionsNetwork" -$repoName = "M365FoundationsCISReport" - -# Function to update GitHub issue -function Update-GitHubTIssue { - param ( - [int]$issueNumber, - [string]$title, - [string]$body, - [string]$owner, - [string]$repositoryName - ) - - # Update the issue using Set-GitHubIssue - Set-GitHubIssue -OwnerName $owner -RepositoryName $repositoryName -Issue $issueNumber -Title $title -Body $body -Label @("documentation", "help wanted", "question") -Confirm:$false -} - -# Load test definitions from CSV -$testDefinitionsPath = ".\source\helper\TestDefinitions.csv" -$testDefinitions = Import-Csv -Path $testDefinitionsPath - -# Fetch existing issues that start with "Rec:" -$existingIssues = Get-GitHubIssue -OwnerName 'CriticalSolutionsNetwork' -RepositoryName 'M365FoundationsCISReport' - -# Create a list to hold matched issues -$matchedIssues = @() -$warnings = @() - -# Iterate over each existing issue -$existingIssues | ForEach-Object { - $issueNumber = $_.Number - $issueTitle = $_.Title - $issueBody = $_.Body - - # Extract the rec number from the issue title - if ($issueTitle -match "Rec: (\d+\.\d+\.\d+)") { - $rec = $matches[1] - - # Find the matching test definition based on rec number - $testDefinition = $testDefinitions | Where-Object { $_.Rec -eq $rec } - - if ($testDefinition) { - # Create the new issue body - $newIssueBody = @" -# Validation for $($testDefinition.TestFileName) - -## Recommendation Details -- **Recommendation**: $($testDefinition.Rec) -- **Description**: $($testDefinition.RecDescription) -- **ELevel**: $($testDefinition.ELevel) -- **Profile Level**: $($testDefinition.ProfileLevel) -- **CIS Control**: $($testDefinition.CISControl) -- **CIS Description**: $($testDefinition.CISDescription) -- **Implementation Group 1**: $($testDefinition.IG1) -- **Implementation Group 2**: $($testDefinition.IG2) -- **Implementation Group 3**: $($testDefinition.IG3) -- **Automated**: $($testDefinition.Automated) -- **Connection**: $($testDefinition.Connection) - -## [$($testDefinition.TestFileName)](https://github.com/CriticalSolutionsNetwork/M365FoundationsCISReport/blob/main/source/tests/$($testDefinition.TestFileName)) - -## Tasks - -### Validate recommendation details -- [ ] Confirm that the recommendation details are accurate and complete as per the CIS benchmark. - -### Validate test for a pass -- [ ] Confirm that the automated test results align with the manual audit steps outlined in the CIS benchmark. - - Specific conditions to check: - - Condition A: (Detail about what constitutes Condition A) - - Condition B: (Detail about what constitutes Condition B) - - Condition C: (Detail about what constitutes Condition C) - -### Validate test for a fail -- [ ] Confirm that the failure conditions in the automated test are consistent with the manual audit results. - - Specific conditions to check: - - Condition A: (Detail about what constitutes Condition A) - - Condition B: (Detail about what constitutes Condition B) - - Condition C: (Detail about what constitutes Condition C) - -### Add notes and observations -- [ ] Compare the automated audit results with the manual audit steps and provide detailed observations. - - Automated audit produced info consistent with the manual audit test results? (Yes/No) - - Without disclosing any sensitive information, document any discrepancies between the actual output and the expected output. - - Document any error messages, removing any sensitive information before submitting. - - Identify the specific function, line, or section of the script that failed, if known. - - Provide any additional context or observations that might help in troubleshooting. - -If needed, the helpers folder in .\source\helpers contains a CSV to assist with locating the test definition. -"@ - - # Add to matched issues list - $matchedIssues += [PSCustomObject]@{ - IssueNumber = $issueNumber - Title = $issueTitle - NewBody = $newIssueBody - } - } else { - $warnings += "No matching test definition found for Rec: $rec" - } - } else { - $warnings += "No matching rec number found in issue title #$issueNumber" - } -} - -# Display matched issues for confirmation -if ($matchedIssues.Count -gt 0) { - Write-Output "Matched Issues:" - $matchedIssues | ForEach-Object { - Write-Output $_.Title - } - - $confirmation = Read-Host "Do you want to proceed with updating these issues? (yes/no)" - - if ($confirmation -eq 'yes') { - # Update the issues - $matchedIssues | ForEach-Object { - try { - Update-GitHubTIssue -issueNumber $_.IssueNumber -title $_.Title -body $_.NewBody -owner $repoOwner -repositoryName $repoName - Write-Output "Updated issue #$($_.IssueNumber)" - } catch { - Write-Error "Failed to update issue #$($_.IssueNumber): $_" - } - - # Introduce a delay of 2 seconds - Start-Sleep -Seconds 2 - } - } else { - Write-Output "Update canceled by user." - } -} else { - Write-Output "No matched issues found to update." -} - -# Display any warnings that were captured -if ($warnings.Count -gt 0) { - Write-Output "Warnings:" - $warnings | ForEach-Object { - Write-Output $_ - } -} - -# Test command to verify GitHub access -Get-GitHubRepository -OwnerName 'CriticalSolutionsNetwork' -RepositoryName 'M365FoundationsCISReport' - - -######################################################################################### -connect-MgGraph -Scopes "Directory.Read.All", "Domain.Read.All", "Policy.Read.All", "Organization.Read.All" -NoWelcome -# Retrieve the subscribed SKUs -$sub = Get-MgSubscribedSku -All - -# Define the product array -$ProductArray = @( - "Microsoft_Cloud_App_Security_App_Governance_Add_On", - "Defender_Threat_Intelligence", - "THREAT_INTELLIGENCE", - "WIN_DEF_ATP", - "Microsoft_Defender_for_Endpoint_F2", - "DEFENDER_ENDPOINT_P1", - "DEFENDER_ENDPOINT_P1_EDU", - "MDATP_XPLAT", - "MDATP_Server", - "ATP_ENTERPRISE_FACULTY", - "ATA", - "ATP_ENTERPRISE_GOV", - "ATP_ENTERPRISE_USGOV_GCCHIGH", - "THREAT_INTELLIGENCE_GOV", - "TVM_Premium_Standalone", - "TVM_Premium_Add_on", - "ATP_ENTERPRISE", - "Azure_Information_Protection_Premium_P1", - "Azure_Information_Protection_Premium_P2", - "Microsoft_Application_Protection_and_Governance", - "Exchange_Online_Protection", - "Microsoft_365_Defender", - "Cloud_App_Security_Discovery" -) - -# Define the hashtable -$ProductHashTable = @{ - "App governance add-on to Microsoft Defender for Cloud Apps" = "Microsoft_Cloud_App_Security_App_Governance_Add_On" - "Defender Threat Intelligence" = "Defender_Threat_Intelligence" - "Microsoft Defender for Office 365 (Plan 2)" = "THREAT_INTELLIGENCE" - "Microsoft Defender for Endpoint" = "WIN_DEF_ATP" - "Microsoft Defender for Endpoint F2" = "Microsoft_Defender_for_Endpoint_F2" - "Microsoft Defender for Endpoint P1" = "DEFENDER_ENDPOINT_P1" - "Microsoft Defender for Endpoint P1 for EDU" = "DEFENDER_ENDPOINT_P1_EDU" - "Microsoft Defender for Endpoint P2_XPLAT" = "MDATP_XPLAT" - "Microsoft Defender for Endpoint Server" = "MDATP_Server" - "Microsoft Defender for Office 365 (Plan 1) Faculty" = "ATP_ENTERPRISE_FACULTY" - "Microsoft Defender for Identity" = "ATA" - "Microsoft Defender for Office 365 (Plan 1) GCC" = "ATP_ENTERPRISE_GOV" - "Microsoft Defender for Office 365 (Plan 1)_USGOV_GCCHIGH" = "ATP_ENTERPRISE_USGOV_GCCHIGH" - "Microsoft Defender for Office 365 (Plan 2) GCC" = "THREAT_INTELLIGENCE_GOV" - "Microsoft Defender Vulnerability Management" = "TVM_Premium_Standalone" - "Microsoft Defender Vulnerability Management Add-on" = "TVM_Premium_Add_on" - "Microsoft Defender for Office 365 (Plan 1)" = "ATP_ENTERPRISE" - "Azure Information Protection Premium P1" = "Azure_Information_Protection_Premium_P1" - "Azure Information Protection Premium P2" = "Azure_Information_Protection_Premium_P2" - "Microsoft Application Protection and Governance" = "Microsoft_Application_Protection_and_Governance" - "Exchange Online Protection" = "Exchange_Online_Protection" - "Microsoft 365 Defender" = "Microsoft_365_Defender" - "Cloud App Security Discovery" = "Cloud_App_Security_Discovery" -} - -# Reverse the hashtable -$ReverseProductHashTable = @{} -foreach ($key in $ProductHashTable.Keys) { - $ReverseProductHashTable[$ProductHashTable[$key]] = $key -} - -# Loop through each SKU and get the enabled security features -$securityFeatures = foreach ($sku in $sub) { -if ($sku.SkuPartNumber -eq "MDATP_XPLAT_EDU") { -Write-Host "the SKU is: `n$($sku | gm)" - [PSCustomObject]@{ - Skupartnumber = $sku.skupartnumber - AppliesTo = $sku.AppliesTo - ProvisioningStatus = $sku.ProvisioningStatus - ServicePlanId = $sku.ServicePlanId - ServicePlanName = $sku.ServicePlanName - FriendlyName = "Defender P2 for EDU" - } - } - else { - - $sku.serviceplans | Where-Object { $_.serviceplanname -in $ProductArray } | ForEach-Object { - $friendlyName = $ReverseProductHashTable[$_.ServicePlanName] - [PSCustomObject]@{ - Skupartnumber = $sku.skupartnumber - AppliesTo = $_.AppliesTo - ProvisioningStatus = $_.ProvisioningStatus - ServicePlanId = $_.ServicePlanId - ServicePlanName = $_.ServicePlanName - FriendlyName = $friendlyName - } - } - - } - -} - -# Output the security features -$securityFeatures | Format-Table -AutoSize - - - -########## - -# Ensure the ImportExcel module is available - - -# Ensure the ImportExcel module is available -if (-not (Get-Module -ListAvailable -Name ImportExcel)) { - Install-Module -Name ImportExcel -Force -Scope CurrentUser -} - -# Function to wait until the file is available -function Wait-ForFile { - param ( - [string]$FilePath - ) - while (Test-Path -Path $FilePath -PathType Leaf -and -not (Get-Content $FilePath -ErrorAction SilentlyContinue)) { - Start-Sleep -Seconds 1 - } -} - -# Path to the Excel file -$excelFilePath = "C:\Users\dougrios\OneDrive - CRITICALSOLUTIONS NET LLC\Documents\_Tools\Benchies\SKUs.xlsx" - -# Wait for the file to be available - - -# Import the Excel file -$excelData = Import-Excel -Path $excelFilePath - -# Retrieve the subscribed SKUs -$subscribedSkus = Get-MgSubscribedSku -All - -# Define the hashtable with security-related product names -$ProductHashTable = @{ - "App governance add-on to Microsoft Defender for Cloud Apps" = "Microsoft_Cloud_App_Security_App_Governance_Add_On" - "Defender Threat Intelligence" = "Defender_Threat_Intelligence" - "Microsoft Defender for Office 365 (Plan 2)" = "THREAT_INTELLIGENCE" - "Microsoft Defender for Endpoint" = "WIN_DEF_ATP" - "Microsoft Defender for Endpoint F2" = "Microsoft_Defender_for_Endpoint_F2" - "Microsoft Defender for Endpoint P1" = "DEFENDER_ENDPOINT_P1" - "Microsoft Defender for Endpoint P1 for EDU" = "DEFENDER_ENDPOINT_P1_EDU" - "Microsoft Defender for Endpoint P2_XPLAT" = "MDATP_XPLAT" - "Microsoft Defender for Endpoint Server" = "MDATP_Server" - "Microsoft Defender for Office 365 (Plan 1) Faculty" = "ATP_ENTERPRISE_FACULTY" - "Microsoft Defender for Identity" = "ATA" - "Microsoft Defender for Office 365 (Plan 1) GCC" = "ATP_ENTERPRISE_GOV" - "Microsoft Defender for Office 365 (Plan 1)_USGOV_GCCHIGH" = "ATP_ENTERPRISE_USGOV_GCCHIGH" - "Microsoft Defender for Office 365 (Plan 2) GCC" = "THREAT_INTELLIGENCE_GOV" - "Microsoft Defender Vulnerability Management" = "TVM_Premium_Standalone" - "Microsoft Defender Vulnerability Management Add-on" = "TVM_Premium_Add_on" - "Microsoft Defender for Office 365 (Plan 1)" = "ATP_ENTERPRISE" - "Azure Information Protection Premium P1" = "Azure_Information_Protection_Premium_P1" - "Azure Information Protection Premium P2" = "Azure_Information_Protection_Premium_P2" - "Microsoft Application Protection and Governance" = "Microsoft_Application_Protection_and_Governance" - "Exchange Online Protection" = "Exchange_Online_Protection" - "Microsoft 365 Defender" = "Microsoft_365_Defender" - "Cloud App Security Discovery" = "Cloud_App_Security_Discovery" -} - -# Create a hashtable to store the SKU part numbers and their associated security features -$skuSecurityFeatures = @{} - -# Populate the hashtable with data from the Excel file -foreach ($row in $excelData) { - if ($null -ne $row.'String ID' -and $null -ne $row.'Service plans included (friendly names)') { - $skuSecurityFeatures[$row.'String ID'] = $row.'Service plans included (friendly names)' - } -} - -# Display the SKU part numbers and their associated security features -foreach ($sku in $subscribedSkus) { - $skuPartNumber = $sku.SkuPartNumber - if ($skuSecurityFeatures.ContainsKey($skuPartNumber)) { - $securityFeatures = $skuSecurityFeatures[$skuPartNumber] - - # Check if the security feature is in the hashtable - $isSecurityFeature = $ProductHashTable.ContainsKey($securityFeatures) - - if ($isSecurityFeature) { - Write-Output "SKU Part Number: $skuPartNumber" - Write-Output "Security Features: $securityFeatures (Security-related)" - } else { - Write-Output "SKU Part Number: $skuPartNumber" - Write-Output "Security Features: $securityFeatures" - } - Write-Output "----------------------------" - } else { - Write-Output "SKU Part Number: $skuPartNumber" - Write-Output "Security Features: Not Found in Excel" - Write-Output "----------------------------" - } -} diff --git a/tests/Unit/Private/Get-Action.tests.ps1 b/tests/Unit/Private/Get-Action.tests.ps1 new file mode 100644 index 0000000..4a2aa69 --- /dev/null +++ b/tests/Unit/Private/Get-Action.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-ExceededLengthResultDetail.tests.ps1 b/tests/Unit/Private/Get-ExceededLengthResultDetail.tests.ps1 new file mode 100644 index 0000000..4a2aa69 --- /dev/null +++ b/tests/Unit/Private/Get-ExceededLengthResultDetail.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-PrivateFunction.tests.ps1 b/tests/Unit/Private/Get-PrivateFunction.tests.ps1 deleted file mode 100644 index 87bdf1b..0000000 --- a/tests/Unit/Private/Get-PrivateFunction.tests.ps1 +++ /dev/null @@ -1,31 +0,0 @@ -BeforeAll { - $script:dscModuleName = 'M365FoundationsCISReport' - - Import-Module -Name $script:dscModuleName -} - -AfterAll { - # Unload the module being tested so that it doesn't impact any other tests. - Get-Module -Name $script:dscModuleName -All | Remove-Module -Force -} - -Describe Get-PrivateFunction { - Context 'When calling the function with string value' { - It 'Should return a single object' { - InModuleScope -ModuleName $dscModuleName { - $return = Get-PrivateFunction -PrivateData 'string' - - ($return | Measure-Object).Count | Should -Be 1 - } - } - - It 'Should return a string based on the parameter PrivateData' { - InModuleScope -ModuleName $dscModuleName { - $return = Get-PrivateFunction -PrivateData 'string' - - $return | Should -Be 'string' - } - } - } -} - diff --git a/tests/Unit/Private/Initialize-LargeTestTable.tests.ps1 b/tests/Unit/Private/Initialize-LargeTestTable.tests.ps1 new file mode 100644 index 0000000..4a2aa69 --- /dev/null +++ b/tests/Unit/Private/Initialize-LargeTestTable.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/Public/Export-M365SecurityAuditTable.tests.ps1 b/tests/Unit/Public/Export-M365SecurityAuditTable.tests.ps1 new file mode 100644 index 0000000..5998a20 --- /dev/null +++ b/tests/Unit/Public/Export-M365SecurityAuditTable.tests.ps1 @@ -0,0 +1,71 @@ +BeforeAll { + $script:moduleName = '<% $PLASTER_PARAM_ModuleName %>' + + # If the module is not found, run the build task 'noop'. + if (-not (Get-Module -Name $script:moduleName -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null + } + + # Re-import the module using force to get any code changes between runs. + Import-Module -Name $script:moduleName -Force -ErrorAction 'Stop' + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:moduleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + Remove-Module -Name $script:moduleName +} + +Describe Get-Something { + + Context 'Return values' { + BeforeEach { + $return = Get-Something -Data 'value' + } + + It 'Returns a single object' { + ($return | Measure-Object).Count | Should -Be 1 + } + + } + + Context 'Pipeline' { + It 'Accepts values from the pipeline by value' { + $return = 'value1', 'value2' | Get-Something + + $return[0] | Should -Be 'value1' + $return[1] | Should -Be 'value2' + } + + It 'Accepts value from the pipeline by property name' { + $return = 'value1', 'value2' | ForEach-Object { + [PSCustomObject]@{ + Data = $_ + OtherProperty = 'other' + } + } | Get-Something + + + $return[0] | Should -Be 'value1' + $return[1] | Should -Be 'value2' + } + } + + Context 'ShouldProcess' { + It 'Supports WhatIf' { + (Get-Command Get-Something).Parameters.ContainsKey('WhatIf') | Should -Be $true + { Get-Something -Data 'value' -WhatIf } | Should -Not -Throw + } + + + } +} + diff --git a/tests/Unit/Public/Remove-RowsWithEmptyCSVStatus.tests.ps1 b/tests/Unit/Public/Remove-RowsWithEmptyCSVStatus.tests.ps1 new file mode 100644 index 0000000..5998a20 --- /dev/null +++ b/tests/Unit/Public/Remove-RowsWithEmptyCSVStatus.tests.ps1 @@ -0,0 +1,71 @@ +BeforeAll { + $script:moduleName = '<% $PLASTER_PARAM_ModuleName %>' + + # If the module is not found, run the build task 'noop'. + if (-not (Get-Module -Name $script:moduleName -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null + } + + # Re-import the module using force to get any code changes between runs. + Import-Module -Name $script:moduleName -Force -ErrorAction 'Stop' + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:moduleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + Remove-Module -Name $script:moduleName +} + +Describe Get-Something { + + Context 'Return values' { + BeforeEach { + $return = Get-Something -Data 'value' + } + + It 'Returns a single object' { + ($return | Measure-Object).Count | Should -Be 1 + } + + } + + Context 'Pipeline' { + It 'Accepts values from the pipeline by value' { + $return = 'value1', 'value2' | Get-Something + + $return[0] | Should -Be 'value1' + $return[1] | Should -Be 'value2' + } + + It 'Accepts value from the pipeline by property name' { + $return = 'value1', 'value2' | ForEach-Object { + [PSCustomObject]@{ + Data = $_ + OtherProperty = 'other' + } + } | Get-Something + + + $return[0] | Should -Be 'value1' + $return[1] | Should -Be 'value2' + } + } + + Context 'ShouldProcess' { + It 'Supports WhatIf' { + (Get-Command Get-Something).Parameters.ContainsKey('WhatIf') | Should -Be $true + { Get-Something -Data 'value' -WhatIf } | Should -Not -Throw + } + + + } +} +