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/Public/Export-M365SecurityAuditTable.ps1 b/source/Public/Export-M365SecurityAuditTable.ps1 new file mode 100644 index 0000000..057672d --- /dev/null +++ b/source/Public/Export-M365SecurityAuditTable.ps1 @@ -0,0 +1,110 @@ +function Export-M365SecurityAuditTable { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ParameterSetName = "FromAuditResultsSingle")] + [Parameter(Mandatory = $true, ParameterSetName = "FromAuditResultsMultiple")] + [CISAuditResult[]]$AuditResults, + + [Parameter(Mandatory = $true, ParameterSetName = "FromCsvSingle")] + [Parameter(Mandatory = $true, ParameterSetName = "FromCsvMultiple")] + [ValidateScript({ Test-Path $_ -and (Get-Item $_).PSIsContainer -eq $false })] + [string]$CsvPath, + + [Parameter(Mandatory = $false, Position = 1, ParameterSetName = "FromAuditResultsSingle")] + [Parameter(Mandatory = $false, Position = 1, ParameterSetName = "FromCsvSingle")] + [ValidateSet("6.1.2","6.1.3","7.3.4")] + [string]$TestNumber, + + [Parameter(Mandatory = $false, Position = 1, ParameterSetName = "FromAuditResultsMultiple")] + [Parameter(Mandatory = $false, Position = 1, ParameterSetName = "FromCsvMultiple")] + [ValidateSet("6.1.2","6.1.3","7.3.4")] + [string[]]$TestNumbers, + + [Parameter(Mandatory = $true, Position = 2, ParameterSetName = "FromAuditResultsMultiple")] + [Parameter(Mandatory = $true, Position = 2, ParameterSetName = "FromCsvMultiple")] + [Parameter(Mandatory = $false, Position = 2, ParameterSetName = "FromAuditResultsSingle")] + [Parameter(Mandatory = $false, Position = 2, ParameterSetName = "FromCsvSingle")] + [string]$ExportPath + ) + + if ($PSCmdlet.ParameterSetName -like "FromCsv*") { + $AuditResults = Import-Csv -Path $CsvPath | ForEach-Object { + [CISAuditResult]::new( + $_.Status, + $_.ELevel, + $_.ProfileLevel, + [bool]$_.Automated, + $_.Connection, + $_.Rec, + $_.RecDescription, + $_.CISControlVer, + $_.CISControl, + $_.CISDescription, + [bool]$_.IG1, + [bool]$_.IG2, + [bool]$_.IG3, + [bool]$_.Result, + $_.Details, + $_.FailureReason + ) + } + } + + #$script:TestDefinitionsObject = Import-Csv -Path .\source\helper\TestDefinitions.csv + if (-not $TestNumbers -and -not $TestNumber) { + $TestNumbers = "6.1.2","6.1.3","7.3.4" + if (-not $ExportPath) { + Write-Error "ExportPath is required when exporting all test results." + return + } + } + + $results = @() + + $testsToProcess = if ($TestNumber) { @($TestNumber) } else { $TestNumbers } + + foreach ($test in $testsToProcess) { + $auditResult = $AuditResults | Where-Object { $_.Rec -eq $test } + if (-not $auditResult) { + Write-Error "No audit results found for the test number $test." + continue + } + + switch ($test) { + "6.1.3" { + $details = $auditResult.Details + $csv = $details | ConvertFrom-Csv -Delimiter '|' + + 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 + $results += [PSCustomObject]@{ TestNumber = $test; Details = $newObjectDetails } + } + "7.3.4" { + # Placeholder for specific logic for 7.3.4 if needed + } + 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") + 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" + $result.Details | Out-File -FilePath $fileName + } + } + } else { + return $results.Details + } +} \ No newline at end of file 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-MailboxAuditingE5.ps1 b/source/tests/Test-MailboxAuditingE5.ps1 index a332baf..6e65b9d 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,7 +91,12 @@ 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 } @@ -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/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/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 + } + + + } +} +