From 07b95a04fb709eb30db19874f96603f6e2b2cf6d Mon Sep 17 00:00:00 2001 From: Ethan <98476160+ethanb-cisa@users.noreply.github.com> Date: Fri, 16 Dec 2022 11:13:59 -0500 Subject: [PATCH] v0.2.0 release (#24) * v0.2.0 release --- .gitattributes | 1 + .github/workflows/run_opa_tests.yaml | 3 +- .gitignore | 13 +- AllowBasicAuthentication.ps1 | 62 +- PowerShell/ScubaGear/Dependencies.ps1 | 41 + .../Modules/Connection/ConnectHelpers.psm1 | 33 + .../Modules/Connection/Connection.psm1 | 278 +- .../Modules/CreateReport/CreateReport.psm1 | 69 +- .../CreateReport/ParentReportTemplate.html | 12 +- .../Modules/CreateReport/ParentStyle.css | 11 +- .../Modules/CreateReport/ReportTemplate.html | 9 +- .../ScubaGear/Modules/CreateReport/main.css | 29 +- .../ScubaGear/Modules/CreateReport/main.js | 5 + .../ScubaGear/Modules/Orchestrator.psm1 | 699 +- .../Modules/Providers/ExportAADProvider.psm1 | 31 +- .../Providers/ExportDefenderProvider.psm1 | 151 +- .../Modules/Providers/ExportEXOProvider.psm1 | 342 +- .../Providers/ExportOneDriveProvider.psm1 | 29 +- .../ExportPowerPlatformProvider.psm1 | 52 +- .../Providers/ExportSharePointProvider.psm1 | 28 +- .../Providers/ExportTeamsProvider.psm1 | 74 +- .../ProviderHelpers/CommandTracker.psm1 | 65 + .../ProviderHelpers/SPOSiteHelper.psm1 | 39 + PowerShell/ScubaGear/RequiredVersions.ps1 | 93 + PowerShell/ScubaGear/ScubaGear.psd1 | Bin 8554 -> 4120 bytes PowerShell/ScubaGear/ScubaGear.psm1 | 2 + README.md | 310 +- Rego/AADConfig.rego | 6 +- Rego/DefenderConfig.rego | 246 +- Rego/EXOConfig.rego | 145 +- Rego/OneDriveConfig.rego | 85 +- Rego/SharepointConfig.rego | 89 +- Rego/TeamsConfig.rego | 122 +- SetUp.ps1 | 137 +- Testing/Functional/Auto/ExtremeTest.txt | 1956 +++ Testing/Functional/Auto/MinimumTest.txt | 41 + Testing/Functional/Auto/SimpleTest.txt | 6 + .../Functional/RegoCachedProviderTesting.ps1 | 64 + Testing/RunFunctionalTests.ps1 | 471 + Testing/RunUnitTests.ps1 | 218 + Testing/Unit/Rego/AAD/AADConfig2_01_test.rego | 198 + Testing/Unit/Rego/AAD/AADConfig2_02_test.rego | 274 + Testing/Unit/Rego/AAD/AADConfig2_03_test.rego | 222 + Testing/Unit/Rego/AAD/AADConfig2_04_test.rego | 209 + Testing/Unit/Rego/AAD/AADConfig2_05_test.rego | 35 + Testing/Unit/Rego/AAD/AADConfig2_06_test.rego | 47 + Testing/Unit/Rego/AAD/AADConfig2_07_test.rego | 130 + Testing/Unit/Rego/AAD/AADConfig2_08_test.rego | 19 + Testing/Unit/Rego/AAD/AADConfig2_09_test.rego | 250 + Testing/Unit/Rego/AAD/AADConfig2_10_test.rego | 210 + Testing/Unit/Rego/AAD/AADConfig2_11_test.rego | 86 + Testing/Unit/Rego/AAD/AADConfig2_12_test.rego | 78 + Testing/Unit/Rego/AAD/AADConfig2_13_test.rego | 248 + Testing/Unit/Rego/AAD/AADConfig2_14_test.rego | 199 + Testing/Unit/Rego/AAD/AADConfig2_15_test.rego | 80 + Testing/Unit/Rego/AAD/AADConfig2_16_test.rego | 359 + Testing/Unit/Rego/AAD/AADConfig2_17_test.rego | 192 + Testing/Unit/Rego/AAD/AADConfig2_18_test.rego | 133 + .../Defender/DefenderConfig2_01_test.rego | 119 + .../Defender/DefenderConfig2_02_test.rego | 888 ++ .../Defender/DefenderConfig2_03_test.rego | 198 + .../Defender/DefenderConfig2_04_test.rego | 70 + .../Defender/DefenderConfig2_05_test.rego | 1377 ++ .../Defender/DefenderConfig2_06_test.rego | 1278 ++ .../Defender/DefenderConfig2_07_test.rego | 1412 ++ .../Defender/DefenderConfig2_08_test.rego | 466 + .../Defender/DefenderConfig2_09_test.rego | 233 + .../Defender/DefenderConfig2_10_test.rego | 74 + Testing/Unit/Rego/EXO/EXOConfig2_01_test.rego | 102 + Testing/Unit/Rego/EXO/EXOConfig2_02_test.rego | 197 + Testing/Unit/Rego/EXO/EXOConfig2_03_test.rego | 347 + Testing/Unit/Rego/EXO/EXOConfig2_04_test.rego | 335 + Testing/Unit/Rego/EXO/EXOConfig2_05_test.rego | 47 + Testing/Unit/Rego/EXO/EXOConfig2_06_test.rego | 106 + Testing/Unit/Rego/EXO/EXOConfig2_07_test.rego | 88 + Testing/Unit/Rego/EXO/EXOConfig2_08_test.rego | 35 + Testing/Unit/Rego/EXO/EXOConfig2_09_test.rego | 51 + Testing/Unit/Rego/EXO/EXOConfig2_10_test.rego | 51 + Testing/Unit/Rego/EXO/EXOConfig2_11_test.rego | 51 + Testing/Unit/Rego/EXO/EXOConfig2_12_test.rego | 136 + Testing/Unit/Rego/EXO/EXOConfig2_13_test.rego | 49 + Testing/Unit/Rego/EXO/EXOConfig2_14_test.rego | 51 + Testing/Unit/Rego/EXO/EXOConfig2_15_test.rego | 51 + Testing/Unit/Rego/EXO/EXOConfig2_16_test.rego | 35 + Testing/Unit/Rego/EXO/EXOConfig2_17_test.rego | 51 + .../OneDrive/OneDriveConfig2_01_test.rego | 44 + .../OneDrive/OneDriveConfig2_02_test.rego | 105 + .../OneDrive/OneDriveConfig2_03_test.rego | 44 + .../OneDrive/OneDriveConfig2_04_test.rego | 69 + .../OneDrive/OneDriveConfig2_05_test.rego | 44 + .../OneDrive/OneDriveConfig2_06_test.rego | 19 + .../OneDrive/OneDriveConfig2_07_test.rego | 19 + .../PowerPlatformConfig2_01_test.rego | 40 + .../PowerPlatformConfig2_02_test.rego | 293 + .../PowerPlatformConfig2_03_test.rego | 60 + .../PowerPlatformConfig2_04_test.rego | 19 + .../Sharepoint/SharepointConfig2_01_test.rego | 44 + .../Sharepoint/SharepointConfig2_02_test.rego | 44 + .../Sharepoint/SharepointConfig2_03_test.rego | 19 + .../Sharepoint/SharepointConfig2_04_test.rego | 104 + .../Sharepoint/SharepointConfig2_05_test.rego | 59 + .../Unit/Rego/Teams/TeamsConfig2_01_test.rego | 112 + .../Unit/Rego/Teams/TeamsConfig2_02_test.rego | 117 + .../Unit/Rego/Teams/TeamsConfig2_03_test.rego | 179 + .../Unit/Rego/Teams/TeamsConfig2_04_test.rego | 332 + .../Unit/Rego/Teams/TeamsConfig2_05_test.rego | 290 + .../Unit/Rego/Teams/TeamsConfig2_06_test.rego | 94 + .../Unit/Rego/Teams/TeamsConfig2_07_test.rego | 222 + .../Unit/Rego/Teams/TeamsConfig2_08_test.rego | 355 + .../Unit/Rego/Teams/TeamsConfig2_09_test.rego | 165 + .../Unit/Rego/Teams/TeamsConfig2_10_test.rego | 71 + .../Unit/Rego/Teams/TeamsConfig2_11_test.rego | 51 + .../Unit/Rego/Teams/TeamsConfig2_12_test.rego | 35 + .../Unit/Rego/Teams/TeamsConfig2_13_test.rego | 51 + images/scuba-architecture.png | Bin 0 -> 178045 bytes sample-report/BaselineReports.html | Bin 0 -> 12774 bytes .../IndividualReports/AADReport.html | Bin 0 -> 30752 bytes .../IndividualReports/DefenderReport.html | Bin 0 -> 41176 bytes .../IndividualReports/EXOReport.html | Bin 0 -> 34518 bytes .../IndividualReports/OneDriveReport.html | Bin 0 -> 13712 bytes .../IndividualReports/SharePointReport.html | Bin 0 -> 13854 bytes .../IndividualReports/TeamsReport.html | Bin 0 -> 24856 bytes sample-report/IndividualReports/cisa_logo.png | Bin 0 -> 329167 bytes sample-report/ProviderSettingsExport.json | 12338 ++++++++++++++++ sample-report/TestResults.csv | 191 + sample-report/TestResults.json | 2465 +++ utils/RegoCachedProviderTesting.ps1 | 35 + utils/RunSCuBA.ps1 | 35 + utils/ScubaGearSupport.ps1 | 119 + utils/UninstallModules.ps1 | 60 + 130 files changed, 34418 insertions(+), 859 deletions(-) create mode 100644 .gitattributes create mode 100644 PowerShell/ScubaGear/Dependencies.ps1 create mode 100644 PowerShell/ScubaGear/Modules/Connection/ConnectHelpers.psm1 create mode 100644 PowerShell/ScubaGear/Modules/Providers/ProviderHelpers/CommandTracker.psm1 create mode 100644 PowerShell/ScubaGear/Modules/Providers/ProviderHelpers/SPOSiteHelper.psm1 create mode 100644 PowerShell/ScubaGear/RequiredVersions.ps1 create mode 100644 PowerShell/ScubaGear/ScubaGear.psm1 create mode 100644 Testing/Functional/Auto/ExtremeTest.txt create mode 100644 Testing/Functional/Auto/MinimumTest.txt create mode 100644 Testing/Functional/Auto/SimpleTest.txt create mode 100644 Testing/Functional/RegoCachedProviderTesting.ps1 create mode 100644 Testing/RunFunctionalTests.ps1 create mode 100644 Testing/RunUnitTests.ps1 create mode 100644 Testing/Unit/Rego/AAD/AADConfig2_01_test.rego create mode 100644 Testing/Unit/Rego/AAD/AADConfig2_02_test.rego create mode 100644 Testing/Unit/Rego/AAD/AADConfig2_03_test.rego create mode 100644 Testing/Unit/Rego/AAD/AADConfig2_04_test.rego create mode 100644 Testing/Unit/Rego/AAD/AADConfig2_05_test.rego create mode 100644 Testing/Unit/Rego/AAD/AADConfig2_06_test.rego create mode 100644 Testing/Unit/Rego/AAD/AADConfig2_07_test.rego create mode 100644 Testing/Unit/Rego/AAD/AADConfig2_08_test.rego create mode 100644 Testing/Unit/Rego/AAD/AADConfig2_09_test.rego create mode 100644 Testing/Unit/Rego/AAD/AADConfig2_10_test.rego create mode 100644 Testing/Unit/Rego/AAD/AADConfig2_11_test.rego create mode 100644 Testing/Unit/Rego/AAD/AADConfig2_12_test.rego create mode 100644 Testing/Unit/Rego/AAD/AADConfig2_13_test.rego create mode 100644 Testing/Unit/Rego/AAD/AADConfig2_14_test.rego create mode 100644 Testing/Unit/Rego/AAD/AADConfig2_15_test.rego create mode 100644 Testing/Unit/Rego/AAD/AADConfig2_16_test.rego create mode 100644 Testing/Unit/Rego/AAD/AADConfig2_17_test.rego create mode 100644 Testing/Unit/Rego/AAD/AADConfig2_18_test.rego create mode 100644 Testing/Unit/Rego/Defender/DefenderConfig2_01_test.rego create mode 100644 Testing/Unit/Rego/Defender/DefenderConfig2_02_test.rego create mode 100644 Testing/Unit/Rego/Defender/DefenderConfig2_03_test.rego create mode 100644 Testing/Unit/Rego/Defender/DefenderConfig2_04_test.rego create mode 100644 Testing/Unit/Rego/Defender/DefenderConfig2_05_test.rego create mode 100644 Testing/Unit/Rego/Defender/DefenderConfig2_06_test.rego create mode 100644 Testing/Unit/Rego/Defender/DefenderConfig2_07_test.rego create mode 100644 Testing/Unit/Rego/Defender/DefenderConfig2_08_test.rego create mode 100644 Testing/Unit/Rego/Defender/DefenderConfig2_09_test.rego create mode 100644 Testing/Unit/Rego/Defender/DefenderConfig2_10_test.rego create mode 100644 Testing/Unit/Rego/EXO/EXOConfig2_01_test.rego create mode 100644 Testing/Unit/Rego/EXO/EXOConfig2_02_test.rego create mode 100644 Testing/Unit/Rego/EXO/EXOConfig2_03_test.rego create mode 100644 Testing/Unit/Rego/EXO/EXOConfig2_04_test.rego create mode 100644 Testing/Unit/Rego/EXO/EXOConfig2_05_test.rego create mode 100644 Testing/Unit/Rego/EXO/EXOConfig2_06_test.rego create mode 100644 Testing/Unit/Rego/EXO/EXOConfig2_07_test.rego create mode 100644 Testing/Unit/Rego/EXO/EXOConfig2_08_test.rego create mode 100644 Testing/Unit/Rego/EXO/EXOConfig2_09_test.rego create mode 100644 Testing/Unit/Rego/EXO/EXOConfig2_10_test.rego create mode 100644 Testing/Unit/Rego/EXO/EXOConfig2_11_test.rego create mode 100644 Testing/Unit/Rego/EXO/EXOConfig2_12_test.rego create mode 100644 Testing/Unit/Rego/EXO/EXOConfig2_13_test.rego create mode 100644 Testing/Unit/Rego/EXO/EXOConfig2_14_test.rego create mode 100644 Testing/Unit/Rego/EXO/EXOConfig2_15_test.rego create mode 100644 Testing/Unit/Rego/EXO/EXOConfig2_16_test.rego create mode 100644 Testing/Unit/Rego/EXO/EXOConfig2_17_test.rego create mode 100644 Testing/Unit/Rego/OneDrive/OneDriveConfig2_01_test.rego create mode 100644 Testing/Unit/Rego/OneDrive/OneDriveConfig2_02_test.rego create mode 100644 Testing/Unit/Rego/OneDrive/OneDriveConfig2_03_test.rego create mode 100644 Testing/Unit/Rego/OneDrive/OneDriveConfig2_04_test.rego create mode 100644 Testing/Unit/Rego/OneDrive/OneDriveConfig2_05_test.rego create mode 100644 Testing/Unit/Rego/OneDrive/OneDriveConfig2_06_test.rego create mode 100644 Testing/Unit/Rego/OneDrive/OneDriveConfig2_07_test.rego create mode 100644 Testing/Unit/Rego/PowerPlatform/PowerPlatformConfig2_01_test.rego create mode 100644 Testing/Unit/Rego/PowerPlatform/PowerPlatformConfig2_02_test.rego create mode 100644 Testing/Unit/Rego/PowerPlatform/PowerPlatformConfig2_03_test.rego create mode 100644 Testing/Unit/Rego/PowerPlatform/PowerPlatformConfig2_04_test.rego create mode 100644 Testing/Unit/Rego/Sharepoint/SharepointConfig2_01_test.rego create mode 100644 Testing/Unit/Rego/Sharepoint/SharepointConfig2_02_test.rego create mode 100644 Testing/Unit/Rego/Sharepoint/SharepointConfig2_03_test.rego create mode 100644 Testing/Unit/Rego/Sharepoint/SharepointConfig2_04_test.rego create mode 100644 Testing/Unit/Rego/Sharepoint/SharepointConfig2_05_test.rego create mode 100644 Testing/Unit/Rego/Teams/TeamsConfig2_01_test.rego create mode 100644 Testing/Unit/Rego/Teams/TeamsConfig2_02_test.rego create mode 100644 Testing/Unit/Rego/Teams/TeamsConfig2_03_test.rego create mode 100644 Testing/Unit/Rego/Teams/TeamsConfig2_04_test.rego create mode 100644 Testing/Unit/Rego/Teams/TeamsConfig2_05_test.rego create mode 100644 Testing/Unit/Rego/Teams/TeamsConfig2_06_test.rego create mode 100644 Testing/Unit/Rego/Teams/TeamsConfig2_07_test.rego create mode 100644 Testing/Unit/Rego/Teams/TeamsConfig2_08_test.rego create mode 100644 Testing/Unit/Rego/Teams/TeamsConfig2_09_test.rego create mode 100644 Testing/Unit/Rego/Teams/TeamsConfig2_10_test.rego create mode 100644 Testing/Unit/Rego/Teams/TeamsConfig2_11_test.rego create mode 100644 Testing/Unit/Rego/Teams/TeamsConfig2_12_test.rego create mode 100644 Testing/Unit/Rego/Teams/TeamsConfig2_13_test.rego create mode 100644 images/scuba-architecture.png create mode 100644 sample-report/BaselineReports.html create mode 100644 sample-report/IndividualReports/AADReport.html create mode 100644 sample-report/IndividualReports/DefenderReport.html create mode 100644 sample-report/IndividualReports/EXOReport.html create mode 100644 sample-report/IndividualReports/OneDriveReport.html create mode 100644 sample-report/IndividualReports/SharePointReport.html create mode 100644 sample-report/IndividualReports/TeamsReport.html create mode 100644 sample-report/IndividualReports/cisa_logo.png create mode 100644 sample-report/ProviderSettingsExport.json create mode 100644 sample-report/TestResults.csv create mode 100644 sample-report/TestResults.json create mode 100644 utils/RegoCachedProviderTesting.ps1 create mode 100644 utils/RunSCuBA.ps1 create mode 100644 utils/ScubaGearSupport.ps1 create mode 100644 utils/UninstallModules.ps1 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..400ef44334 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +**/*.psd1 diff \ No newline at end of file diff --git a/.github/workflows/run_opa_tests.yaml b/.github/workflows/run_opa_tests.yaml index a86f1ba557..0f57646cc6 100644 --- a/.github/workflows/run_opa_tests.yaml +++ b/.github/workflows/run_opa_tests.yaml @@ -2,6 +2,7 @@ name: Run OPA Tests on: # Run tests on each commit, newly opened/reopened PR, and # PR review submission (e.g. approval) + workflow_dispatch: push: paths: - "**.rego" @@ -30,4 +31,4 @@ jobs: run: opa check --strict Rego Testing - name: Run OPA Tests - run: opa test Rego/*.rego Testing/**/*.rego -v + run: opa test Rego/*.rego Testing/Unit/Rego/**/*.rego -v diff --git a/.gitignore b/.gitignore index 7bcac1dd16..e0a6439f24 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,15 @@ /output /example /M365Baseline* -/Reports* \ No newline at end of file +/Reports* +/utils/Reports* +/utils/output +/utils/M365Baseline* + +# IDE +/.vscode + +# Reports +**/M365BaselineConformance* +/Testing/Functional/Reports* +/Testing/Functional/Archive* \ No newline at end of file diff --git a/AllowBasicAuthentication.ps1 b/AllowBasicAuthentication.ps1 index 3eadc07a22..edf93c27f8 100644 --- a/AllowBasicAuthentication.ps1 +++ b/AllowBasicAuthentication.ps1 @@ -1,29 +1,57 @@ #Requires -RunAsAdministrator -# Run this script to enable basic authentication on your local desktop if you get an error when connecting to Exchange Online. -# See README file Troubleshooting section for details. -# -# This script requires administrative privileges on your local desktop and updates a registry key. -# -$regPath = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WinRM\Client' -$regKey = 'AllowBasic' +<# + .SYNOPSIS + Set Registry to allow basic authentication for WinRM Client + + .DESCRIPTION + Run this script to enable basic authentication on your local desktop if you get an error when connecting to Exchange Online. + + .NOTES + See README file Troubleshooting section for details. + This script requires administrative privileges on your local desktop and updates a registry key. +#> + +function Test-RegistryKey { + <# + .SYNOPSIS + Test if registry key exists + #> + param ( + [parameter (Mandatory = $true)] + [ValidateNotNullOrEmpty()]$Path, + [parameter (Mandatory = $true)] + [ValidateNotNullOrEmpty()]$Key + ) -if (Test-Path -LiteralPath $regPath){ try { - $allowBasic = Get-ItemPropertyValue -Path $regPath -Name $regKey -ErrorAction Stop - } - catch [System.Management.Automation.PSArgumentException]{ - Write-Error -Message "Key, $regKey, was not found" + Get-ItemProperty -Path $Path -Name $Key -ErrorAction Stop | Out-Null + return $true } - catch{ - Write-Error -Message "Unexpected error occured attempting to get registry key, $regKey." + catch { + return $false } +} - if ($allowBasic -ne '1'){ +$regPath = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WinRM\Client' +$regKey = 'AllowBasic' + +if (-Not $(Test-Path -LiteralPath $regPath)) { + New-Item -Path $regPath -Force | Out-Null + New-ItemProperty -Path $regPath -Name $regKey | Out-Null +} elseif (-Not $(Test-RegistryKey -Path $regPath -Key $regKey)) { + New-ItemProperty -Path $regPath -Name $regKey | Out-Null +} + +try { + $allowBasic = Get-ItemPropertyValue -Path $regPath -Name $regKey -ErrorAction Stop + + if ($allowBasic -ne '1') { Set-ItemProperty -Path $regPath -Name $regKey -Type DWord -Value '1' } } -else { - Write-Error -Message "Registry path not found: $regPath" +catch { + Write-Error -Message "Unexpected error occured attempting to update registry key, $regKey." } + diff --git a/PowerShell/ScubaGear/Dependencies.ps1 b/PowerShell/ScubaGear/Dependencies.ps1 new file mode 100644 index 0000000000..1616e37d99 --- /dev/null +++ b/PowerShell/ScubaGear/Dependencies.ps1 @@ -0,0 +1,41 @@ +#Requires -Version 5.1 +<# + .SYNOPSIS + This script verifies the required Powershell modules used by the + assessment tool are installed. + .DESCRIPTION + Verifies a supported version of the modules required to support SCuBAGear are installed. +#> + +$RequiredModulesPath = Join-Path -Path $PSScriptRoot -ChildPath "RequiredVersions.ps1" +if (Test-Path -Path $RequiredModulesPath){ + . $RequiredModulesPath +} + +if (!$ModuleList){ + throw "Required modules list is required." +} + +foreach ($Module in $ModuleList) { + $InstalledModuleVersions = Get-Module -ListAvailable -Name $($Module.ModuleName) + $FoundAcceptableVersion = $false + + foreach ($ModuleVersion in $InstalledModuleVersions){ + + if (($ModuleVersion.Version -ge $Module.ModuleVersion) -and ($ModuleVersion.Version -le $Module.MaximumVersion)){ + $FoundAcceptableVersion = $true + break; + } + } + + if (-not $FoundAcceptableVersion) { + throw [System.IO.FileNotFoundException] "No acceptable installed version found for module: $($Module.ModuleName) + Required Min Version: $($Module.ModuleVersion) | Max Version: $($Module.MaximumVersion) + Run Get-InstalledModule to see a list of currently installed modules + Run SetUp.ps1 or Install-Module $($Module.ModuleName) -force to install the latest version of $($Module.ModuleName)" + } +} + + + + diff --git a/PowerShell/ScubaGear/Modules/Connection/ConnectHelpers.psm1 b/PowerShell/ScubaGear/Modules/Connection/ConnectHelpers.psm1 new file mode 100644 index 0000000000..ff4d63e269 --- /dev/null +++ b/PowerShell/ScubaGear/Modules/Connection/ConnectHelpers.psm1 @@ -0,0 +1,33 @@ +function Connect-EXOHelper { + <# + .Description + This function is used for assisting in connecting to different M365 Environments for EXO. + .Functionality + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidateSet("commercial", "gcc", "gcchigh", "dod", IgnoreCase = $false)] + [string] + $M365Environment + ) + switch ($M365Environment) { + {($_ -eq "commercial") -or ($_ -eq "gcc")} { + Connect-ExchangeOnline -ShowBanner:$false -ErrorAction "Stop" | Out-Null + } + "gcchigh" { + Connect-ExchangeOnline -ShowBanner:$false -ExchangeEnvironmentName "O365USGovGCCHigh" -ErrorAction "Stop" | Out-Null + } + "dod" { + Connect-ExchangeOnline -ShowBanner:$false -ExchangeEnvironmentName "O365USGovDoD" -ErrorAction "Stop" | Out-Null + } + default { + throw "Unsupported or invalid M365Environment argument" + } + } +} + +Export-ModuleMember -Function @( + 'Connect-EXOHelper' +) \ No newline at end of file diff --git a/PowerShell/ScubaGear/Modules/Connection/Connection.psm1 b/PowerShell/ScubaGear/Modules/Connection/Connection.psm1 index fcbfb58437..256d8b040f 100644 --- a/PowerShell/ScubaGear/Modules/Connection/Connection.psm1 +++ b/PowerShell/ScubaGear/Modules/Connection/Connection.psm1 @@ -7,89 +7,271 @@ function Connect-Tenant { .Functionality Internal #> + [CmdletBinding()] param ( - [Parameter(Mandatory=$true)] + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", "onedrive", IgnoreCase = $false)] [string[]] $ProductNames, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [ValidateSet("commercial", "gcc", "gcchigh", "dod", IgnoreCase = $false)] [string] - $Endpoint + $M365Environment ) + Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "ConnectHelpers.psm1") # Prevent duplicate sign ins $EXOAuthRequired = $true $SPOAuthRequired = $true $AADAuthRequired = $true + $ProdAuthFailed = @() + $N = 0 $Len = $ProductNames.Length foreach ($Product in $ProductNames) { $N += 1 $Percent = $N*100/$Len - Write-Progress -Activity "Authenticating to each service" -Status "Authenticating to $($Product); $($n) of $($Len) Products authenticated to." -PercentComplete $Percent - switch ($Product) { - {($_ -eq "exo") -or ($_ -eq "defender")} { - if ($EXOAuthRequired) { - Connect-ExchangeOnline -ShowBanner:$false | Out-Null - Write-Verbose "Defender will require a sign in every single run regardless of what the LogIn parameter is set" - $EXOAuthRequired = $false + $ProgressParams = @{ + 'Activity' = "Authenticating to each Product"; + 'Status' = "Authenticating to $($Product); $($N) of $($Len) Products authenticated to."; + 'PercentComplete' = $Percent; + } + Write-Progress @ProgressParams + try { + switch ($Product) { + "aad" { + $GraphScopes = ( + 'User.Read.All', + 'Policy.Read.All', + 'Organization.Read.All', + 'UserAuthenticationMethod.Read.All', + 'RoleManagement.Read.Directory', + 'GroupMember.Read.All', + 'Directory.Read.All' + ) + $GraphParams = @{ + 'Scopes' = $GraphScopes; + 'ErrorAction' = 'Stop'; + } + switch ($M365Environment) { + {($_ -eq "commercial") -or ($_ -eq "gcc")} { + # Sanity check + $GraphParams = @{ + 'Scopes' = $GraphScopes; + 'ErrorAction' = 'Stop'; + } + } + "gcchigh" { + $GraphParams = $GraphParams + @{'Environment' = "USGov";} + } + "dod" { + $GraphParams = $GraphParams + @{'Environment' = "USGovDoD";} + } + default { + throw "Unsupported or invalid M365Environment argument" + } + } + Connect-MgGraph @GraphParams | Out-Null + Select-MgProfile -Name "Beta" -ErrorAction "Stop" | Out-Null + $AADAuthRequired = $false } - } - "aad" { - Connect-MgGraph -Scopes User.Read.All, Policy.Read.All, Organization.Read.All, UserAuthenticationMethod.Read.All, RoleManagement.Read.Directory, GroupMember.Read.All, Policy.ReadWrite.AuthenticationMethod, Directory.Read.All -ErrorAction Stop | Out-Null - Select-MgProfile Beta | Out-Null - $AADAuthRequired = $false - } - "powerplatform"{ - if (!$Endpoint) { - Write-Output "Power Platform needs an endpoint please specify one as a script arg" + {($_ -eq "exo") -or ($_ -eq "defender")} { + if ($EXOAuthRequired) { + # Moved switch to Connect-Helpers for Defender Provider use + Connect-EXOHelper -M365Environment $M365Environment + Write-Verbose "Defender will require a sign in every single run regardless of what the LogIn parameter is set" + $EXOAuthRequired = $false + } } - else { - Add-PowerAppsAccount -Endpoint $Endpoint | Out-Null + "powerplatform" { + $AddPowerAppsParams = @{ + 'ErrorAction' = 'Stop'; + } + switch ($M365Environment) { + "commercial" { + $AddPowerAppsParams = $AddPowerAppsParams + @{'Endpoint'='prod';} + } + "gcc" { + $AddPowerAppsParams = $AddPowerAppsParams + @{'Endpoint'='usgov';} + } + "gcchigh" { + $AddPowerAppsParams = $AddPowerAppsParams + @{'Endpoint'='usgovhigh';} + } + "dod" { + $AddPowerAppsParams = $AddPowerAppsParams + @{'Endpoint'='dod';} + } + default { + throw "Unsupported or invalid M365Environment argument" + } + } + Add-PowerAppsAccount @AddPowerAppsParams | Out-Null } - } - {($_ -eq "onedrive") -or ($_ -eq "sharepoint")} { - if ($AADAuthRequired) { - Connect-MgGraph | Out-Null - Select-MgProfile Beta | Out-Null - $AADAuthRequired = $false + {($_ -eq "onedrive") -or ($_ -eq "sharepoint")} { + if ($AADAuthRequired) { + $LimitedGraphParams = @{ + 'ErrorAction' = 'Stop'; + } + switch ($M365Environment) { + {($_ -eq "commercial") -or ($_ -eq "gcc")} { + $LimitedGraphParams = @{ + 'ErrorAction' = 'Stop'; + } + } + "gcchigh" { + $LimitedGraphParams = $LimitedGraphParams + @{'Environment' = "USGov";} + } + "dod" { + $LimitedGraphParams = $LimitedGraphParams + @{'Environment' = "USGovDoD";} + } + default { + throw "Unsupported or invalid M365Environment argument" + } + } + Connect-MgGraph @LimitedGraphParams | Out-Null + Select-MgProfile -Name "Beta" -ErrorAction "Stop" | Out-Null + $AADAuthRequired = $false + } + if ($SPOAuthRequired) { + $InitialDomain = (Get-MgOrganization).VerifiedDomains | Where-Object {$_.isInitial} + $InitialDomainPrefix = $InitialDomain.Name.split(".")[0] + $SPOParams = @{ + 'ErrorAction' = 'Stop'; + } + switch ($M365Environment) { + {($_ -eq "commercial") -or ($_ -eq "gcc")} { + $SPOParams = $SPOParams + @{ + 'Url'= "https://$($InitialDomainPrefix)-admin.sharepoint.com"; + } + } + "gcchigh" { + $SPOParams = $SPOParams + @{ + 'Url'= "https://$($InitialDomainPrefix)-admin.sharepoint.us"; + 'Region' = "ITAR"; + } + } + "dod" { + $SPOParams = $SPOParams + @{ + 'Url'= "https://$($InitialDomainPrefix)-admin.sharepoint-mil.us"; + 'Region' = "ITAR"; + } + } + default { + throw "Unsupported or invalid M365Environment argument" + } + } + Connect-SPOService @SPOParams | Out-Null + $SPOAuthRequired = $false + } } - if ($SPOAuthRequired) { - $InitialDomain = (Get-MgOrganization).VerifiedDomains | Where-Object {$_.isInitial} - $InitialDomainPrefix = $InitialDomain.Name.split(".")[0] - Connect-SPOService -Url "https://$($InitialDomainPrefix)-admin.sharepoint.com" | Out-Null - $SPOAuthRequired = $false + "teams" { + $TeamsParams = @{'ErrorAction'= 'Stop'} + switch ($M365Environment) { + {($_ -eq "commercial") -or ($_ -eq "gcc")} { + $TeamsParams = @{'ErrorAction'= 'Stop'} # sanity check + } + "gcchigh" { + $TeamsParams = $TeamsParams + @{'TeamsEnvironmentName'= 'TeamsGCCH';} + } + "dod" { + $TeamsParams = $TeamsParams + @{'TeamsEnvironmentName'= 'TeamsDOD';} + } + default { + throw "Unsupported or invalid M365Environment argument" + } + } + Connect-MicrosoftTeams @TeamsParams | Out-Null + } + default { + Write-Error -Message "Invalid ProductName argument" } - } - "teams" { - Connect-MicrosoftTeams | Out-Null - } - default { - Write-Error -Message "Invalid ProductName argument" } } + catch { + Write-Error "Error establishing a connection with $($Product). $($_)" + $ProdAuthFailed += $Product + Write-Warning "$($Product) will be omitted from the output because of failed authentication" + } } Write-Progress -Activity "Authenticating to each service" -Status "Ready" -Completed + $ProdAuthFailed } -function Disconnect-Tenant { +function Disconnect-SCuBATenant { <# - .Description - This function disconnects the various PowerShell module sessions from the - M365 Tenant. Useful to disconnect then connect to other M365 tenants - Currently Disconect-MgGraph is buggy and may not disconnect properly. + .SYNOPSIS + Disconnect all active M365 connection sessions made by ScubaGear + .DESCRIPTION + Forces disconnect of all outstanding open sessions associated with + M365 product APIs within the current PowerShell session. + Best used after an ScubaGear run to ensure a new tenant connection is + used for future ScubaGear runs. + .Parameter ProductNames + A list of one or more M365 shortened product names this function will disconnect from. By default this function will disconnect from all possible products ScubaGear can run against. + .EXAMPLE + Disconnect-SCuBATenant + .EXAMPLE + Disconnect-SCuBATenant -ProductNames teams + .EXAMPLE + Disconnect-SCuBATenant -ProductNames aad, exo .Functionality Public #> - Disconnect-MicrosoftTeams # Teams - Disconnect-MgGraph # AAD - Disconnect-ExchangeOnline -Confirm:$false -InformationAction Ignore -ErrorAction SilentlyContinue | Out-Null # Exchange and Defender - Remove-PowerAppsAccount # Power Platform - Disconnect-SPOService # OneDrive and Sharepoint + [CmdletBinding()] + param( + [ValidateSet("aad", "defender", "exo", "onedrive","powerplatform", "sharepoint", "teams", IgnoreCase = $false)] + [string[]] + $ProductNames = @("aad", "defender", "exo", "onedrive", "powerplatform", "sharepoint", "teams") + ) + $ErrorActionPreference = "SilentlyContinue" + + try { + $N = 0 + $Len = $ProductNames.Length + + foreach ($Product in $ProductNames) { + $N += 1 + $Percent = $N*100/$Len + Write-Progress -Activity "Disconnecting from each service" -Status "Disconnecting from $($Product); $($n) of $($Len) disconnected." -PercentComplete $Percent + Write-Verbose "Disconnecting from $Product." + if (($Product -eq "aad") -or ($Product -eq "onedrive") -or ($Product -eq "sharepoint")) { + Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null + + if($Product -eq "sharepoint") { + Disconnect-SPOService -ErrorAction SilentlyContinue + } + } + elseif ($Product -eq "teams") { + Disconnect-MicrosoftTeams -Confirm:$false -ErrorAction SilentlyContinue + } + elseif ($Product -eq "powerplatform") { + Remove-PowerAppsAccount -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + } + elseif (($Product -eq "exo") -or ($Product -eq "defender")) { + Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue -InformationAction SilentlyContinue | Out-Null + } + else { + Write-Warning "Product $Product not recognized, skipping..." + } + } + Write-Progress -Activity "Disconnecting from each service" -Status "Done" -Completed + + } catch [System.InvalidOperationException] { + # Suppress error due to disconnect from service with no active connection + continue + } catch { + Write-Error "ERRROR: Could not disconnect from $Product`n$($Error[0]): " + } finally { + $ErrorActionPreference = "Continue" + } + } Export-ModuleMember -Function @( 'Connect-Tenant', - 'Disconnect-Tenant' + 'Disconnect-SCuBATenant' ) diff --git a/PowerShell/ScubaGear/Modules/CreateReport/CreateReport.psm1 b/PowerShell/ScubaGear/Modules/CreateReport/CreateReport.psm1 index 9b4830398f..27657c9a47 100644 --- a/PowerShell/ScubaGear/Modules/CreateReport/CreateReport.psm1 +++ b/PowerShell/ScubaGear/Modules/CreateReport/CreateReport.psm1 @@ -17,50 +17,77 @@ function New-Report { [string] $FullName, - # The location to save the html report in. Defaults to current directory. + # The location to save the html report in. [Parameter(Mandatory=$true)] + [ValidateScript({Test-Path -PathType Container $_})] [string] $IndividualReportPath, - # The location to save the html report in. Defaults to current directory. + # The location to save the html report in. [Parameter(Mandatory=$true)] + [ValidateScript({Test-Path -PathType Container $_})] [string] - $OutPath + $OutPath, + + [Parameter(Mandatory=$true)] + [string] + $OutProviderFileName, + + [Parameter(Mandatory=$true)] + [string] + $OutRegoFileName ) $FileName = Join-Path -Path $PSScriptRoot -ChildPath "BaselineTitles.json" $AllTitles = Get-Content $FileName | ConvertFrom-Json $Titles = $AllTitles.$BaselineName -$FileName = Join-Path -Path $OutPath -ChildPath "TestResults.json" -$TestResults = Get-Content $FileName | ConvertFrom-Json - -$FileName = Join-Path -Path $OutPath -ChildPath "ProviderSettingsExport.json" +$FileName = Join-Path -Path $OutPath -ChildPath "$($OutProviderFileName).json" $SettingsExport = Get-Content $FileName | ConvertFrom-Json +$FileName = Join-Path -Path $OutPath -ChildPath "$($OutRegoFileName).json" +$TestResults = Get-Content $FileName | ConvertFrom-Json + $Fragments = @() $MetaData += [pscustomobject]@{ - "Tenant Name"= $SettingsExport.tenant_details.DisplayName; - "Report Date"=$SettingsExport.date; - "Baseline Version"=$SettingsExport.baseline_version; - "Module Version"=$SettingsExport.module_version + "Tenant Display Name" = $SettingsExport.tenant_details.DisplayName; + "Report Date" = $SettingsExport.date; + "Baseline Version" = $SettingsExport.baseline_version; + "Module Version" = $SettingsExport.module_version } -$Fragments += $MetaData | ConvertTo-HTML -Fragment - +$MetaDataTable = $MetaData | ConvertTo-HTML -Fragment +$MetaDataTable = $MetaDataTable -replace '^(.*?)','
' +$Fragments += $MetaDataTable $ReportSummary = @{ "Warnings" = 0; "Failures" = 0; "Passes" = 0; "Manual" = 0; + "Errors" = 0; "Date" = $SettingsExport.date; } -ForEach ($Title in $Titles) { +foreach ($Title in $Titles) { $Fragment = @() - ForEach ($test in $TestResults | Where-Object -Property Control -eq $Title.Number) { - if ($test.RequirementMet) { + foreach ($test in $TestResults | Where-Object -Property Control -eq $Title.Number) { + $MissingCommands = @() + + if ($SettingsExport."$($BaselineName)_successful_commands" -or $SettingsExport."$($BaselineName)_unsuccessful_commands") { + # If neither of these keys are present, it means the provider for that baseline + # hasn't been updated to the updated error handling method. This check + # here ensures backwards compatibility until all providers are udpated. + $MissingCommands = $test.Commandlet | Where-Object {$SettingsExport."$($BaselineName)_successful_commands" -notcontains $_} + } + + if ($MissingCommands.Count -gt 0) { + $Result = "Error" + $ReportSummary.Errors += 1 + $MissingString = $MissingCommands -Join ", " + $test.ReportDetails = "This test depends on the following command(s) which did not execute successfully: $($MissingString). See terminal output for more details." + } + elseif ($test.RequirementMet) { $Result = "Pass" $ReportSummary.Passes += 1 } @@ -90,11 +117,21 @@ ForEach ($Title in $Titles) { } $Title = "$($FullName) Baseline Report" +$AADWarning = "

Note: Conditional Access Policy exclusions and additional policy conditions +may limit a policy's scope more narrowly than desired. Recommend reviewing matching policies +against the baseline statement to ensure a match between intent and implementation.

" +$NoWarning = "


" Add-Type -AssemblyName System.Web $ReporterPath = $PSScriptRoot $ReportHTML = Get-Content $(Join-Path -Path $ReporterPath -ChildPath "ReportTemplate.html") $ReportHTML = $ReportHTML.Replace("{TITLE}", $Title) +if ($BaselineName -eq "aad") { + $ReportHTML = $ReportHTML.Replace("{AADWARNING}", $AADWarning) +} +else { + $ReportHTML = $ReportHTML.Replace("{AADWARNING}", $NoWarning) +} $MainCSS = Get-Content $(Join-Path -Path $ReporterPath -ChildPath "main.css") $ReportHTML = $ReportHTML.Replace("{MAIN_CSS}", "") diff --git a/PowerShell/ScubaGear/Modules/CreateReport/ParentReportTemplate.html b/PowerShell/ScubaGear/Modules/CreateReport/ParentReportTemplate.html index cca2b36317..8c5c38758b 100644 --- a/PowerShell/ScubaGear/Modules/CreateReport/ParentReportTemplate.html +++ b/PowerShell/ScubaGear/Modules/CreateReport/ParentReportTemplate.html @@ -9,14 +9,18 @@
Return to the report summary -

Secure Configuration Baselines

+

SCuBA M365 Security Baseline Conformance Reports

-

{TENANT_NAME} tenant

+ {TENANT_DETAILS} +

{TABLES}
diff --git a/PowerShell/ScubaGear/Modules/CreateReport/ParentStyle.css b/PowerShell/ScubaGear/Modules/CreateReport/ParentStyle.css index 516437c0e4..1ecf40e7ae 100644 --- a/PowerShell/ScubaGear/Modules/CreateReport/ParentStyle.css +++ b/PowerShell/ScubaGear/Modules/CreateReport/ParentStyle.css @@ -41,7 +41,16 @@ a.individual_reports:active { text-decoration: none; } +table.tenantdata tr:first-child { + color: black; +} + .failure { background-color: #deb8b8; } .warning { background-color: #fff7d6; } .pass { background-color: #d5ebd5; } -.manual { background-color: #ebebf2; } \ No newline at end of file +.manual { background-color: #ebebf2; } + +.error { + background-color: #deb8b8; + color: #d10000; +} \ No newline at end of file diff --git a/PowerShell/ScubaGear/Modules/CreateReport/ReportTemplate.html b/PowerShell/ScubaGear/Modules/CreateReport/ReportTemplate.html index 53777dd2b8..938e9bac92 100644 --- a/PowerShell/ScubaGear/Modules/CreateReport/ReportTemplate.html +++ b/PowerShell/ScubaGear/Modules/CreateReport/ReportTemplate.html @@ -9,10 +9,15 @@
Return to the report summary -

Secure Configuration Baselines

+

{TITLE}

+

{AADWARNING}

{TABLES}
- \ No newline at end of file + diff --git a/PowerShell/ScubaGear/Modules/CreateReport/main.css b/PowerShell/ScubaGear/Modules/CreateReport/main.css index 0d5b1bd673..41f48127a6 100644 --- a/PowerShell/ScubaGear/Modules/CreateReport/main.css +++ b/PowerShell/ScubaGear/Modules/CreateReport/main.css @@ -18,6 +18,22 @@ h3 { color: #005288; } +h4 { + text-align: center; + justify-content: start; + font-size: 10px; + font-family: Arial, Helvetica, sans-serif; + color: #ee4e04; + margin-left:20%; + margin-right: 20%; + margin-bottom:5px; +} + + +.links { + display: flex; +} + header { width: 1000px; margin: auto; @@ -28,17 +44,22 @@ header { align-items: end; padding: 5px; } + header h3 { - width: 150px; - text-align: right; + padding: 10px; + text-align: center; border-bottom: 5px solid rgba(0, 0, 0, 0); color: #005288; + display: table-cell; + vertical-align: bottom; } + header a { text-decoration: none; } + header h3:hover { - border-bottom: 5px solid #005288; + border-bottom: 5px solid #005288; } td { @@ -77,4 +98,4 @@ h2 { img { width: 100px; -} \ No newline at end of file +} diff --git a/PowerShell/ScubaGear/Modules/CreateReport/main.js b/PowerShell/ScubaGear/Modules/CreateReport/main.js index 7212b7b8eb..aa2e8e5ae3 100644 --- a/PowerShell/ScubaGear/Modules/CreateReport/main.js +++ b/PowerShell/ScubaGear/Modules/CreateReport/main.js @@ -16,6 +16,11 @@ function colorRows() { else if (rows[i].children[2].innerHTML.includes("3rd Party")) { rows[i].style.background = "#ebebf2"; } + else if (rows[i].children[1].innerHTML.includes("Error")) { + rows[i].style.background = "#deb8b8"; + rows[i].querySelectorAll('td')[1].style.borderColor = "black"; + rows[i].querySelectorAll('td')[1].style.color = "#d10000"; + } } } diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index 8770145270..9c3121123f 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -1,107 +1,236 @@ function Invoke-SCuBA { <# + .SYNOPSIS + Execute the SCuBAGear tool security baselines for specified M365 products. .Description - This is the orchestrator function that runs the Providers, Rego, and Report creation all in one - PowerShell script call + This is the main function that runs the Providers, Rego, and Report creation all in one PowerShell script call. .Parameter ProductNames - Which Baseline Names to run their respective Providers and Rego tests - .Parameter Endpoint - The Endpoint parameter for PowerPlatform authentication + A list of one or more M365 shortened product names that the tool will assess when it is executed. Acceptable product name values are listed below. + To assess Azure Active Directory you would enter the value aad. + To assess Exchange Online you would enter exo and so forth. + - Azure Active Directory: aad + - Defender for Office 365: defender + - Exchange Online: exo + - OneDrive: onedrive + - MS Power Platform: powerplatform + - SharePoint Online: sharepoint + - MS Teams: teams. + Use '*' to run all baselines. + .Parameter M365Environment + This parameter is used to authenticate to the different commercial/government environments. + Valid values include "commercial", "gcc", "gcchigh", or "dod". + - For M365 tenants with E3/E5 licenses enter the value **"commercial"**. + - For M365 Government Commercial Cloud tenants with G3/G5 licenses enter the value **"gcc"**. + - For M365 Government Commercial Cloud High tenants enter the value **"gcchigh"**. + - For M365 Department of Defense tenants enter the value **"dod"**. + Default value is 'commercial'. .Parameter OPAPath - Path to the OPA executuable + The folder location of the OPA Rego executable file. + The OPA Rego executable embedded with this project is located in the project's root folder. + If you want to execute the tool using a version of OPA Rego located in another folder, + then customize the variable value with the full path to the alternative OPA Rego exe file. .Parameter LogIn - Set $true to authenticate yourself to a tenant or if you are already authenticated set to $false + A `$true` or `$false` variable that if set to `$true` + will prompt you to provide credentials if you want to establish a connection + to the specified M365 products in the **$ProductNames** variable. + For most use cases, leave this variable to be `$true`. + A connection is established in the current PowerShell terminal session with the first authentication. + If you want to run another verification in the same PowerShell session simply set + this variable to be `$false` to bypass the reauthenticating in the same session. Default is $true. + Note: defender will ask for authentication even if this variable is set to `$false` + .Parameter Version + Will output the current ScubaGear version to the terminal without running this cmdlet. + .Parameter OutPath + The folder path where both the output JSON and the HTML report will be created. + The folder will be created if it does not exist. Defaults to current directory. + .Parameter OutFolderName + The name of the folder in OutPath where both the output JSON and the HTML report will be created. + Defaults to "M365BaselineConformance". The client's local timestamp will be appended. + .Parameter OutProviderFileName + The name of the Provider output JSON created in the folder created in OutPath. + Defaults to "ProviderSettingsExport". + .Parameter OutRegoFileName + The name of the Rego output JSON and CSV created in the folder created in OutPath. + Defaults to "TestResults". + .Parameter OutReportName + The name of the main html file page created in the folder created in OutPath. + Defaults to "BaselineReports". + .Parameter DisconnectOnExit + Set switch to disconnect all active connections on exit from ScubaGear (default: $false) .Example - Invoke-SCuBA -LogIn $True -ProductNames @("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", "onedrive") -Endpoint "usgov" -OPAPath "./" -OutPath output + Invoke-SCuBA + Run an assessment against by default a commercial M365 Tenant against the + Azure Active Directory, Exchange Online, Microsoft Defender, One Drive, SharePoint Online, and Microsoft Teams + security baselines. The output will stored in the current directory in a folder called M365BaselineConformaance_*. .Example - Invoke-SCuBA -LogIn $False -ProductNames @("powerplatform", "exo") -Endpoint "prod" -OPAPath "./" -OutPath "/Reports" + Invoke-SCuBA -Version + This example returns the version of SCuBAGear. + .Example + Invoke-SCuBA -ProductNames aad, defender -OPAPath . -OutPath . + The example will run the tool against the Azure Active Directory, and Defender security + baselines. + .Example + Invoke-SCuBA -ProductNames * -M365Environment dod -OPAPath . -OutPath . + This example will run the tool against all available security baselines with the + 'dod' teams endpoint. + .Example + Invoke-SCuBA -ProductNames aad,exo -M365Environment gcc -OPAPath . -OutPath . -DisconnectOnExit + Run the tool against Azure Active Directory and Exchange Online security + baselines, disconnecting connections for those products when complete. .Functionality Public #> [CmdletBinding()] param ( - [Parameter(Mandatory=$true)] - [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", "onedrive", IgnoreCase = $false)] + [Parameter(Mandatory = $false, ParameterSetName = 'Report')] + [ValidateNotNullOrEmpty()] + [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", "onedrive", '*', IgnoreCase = $false)] [string[]] - $ProductNames, + $ProductNames = @("teams", "exo", "defender", "aad", "sharepoint", "onedrive"), + [Parameter(Mandatory = $false, ParameterSetName = 'Report')] + [ValidateSet("commercial", "gcc", "gcchigh", "dod", IgnoreCase = $false)] + [ValidateNotNullOrEmpty()] [string] - $Endpoint = "", + $M365Environment = "commercial", - [Parameter(Mandatory=$true)] + [Parameter(Mandatory = $false, ParameterSetName = 'Report')] + [ValidateScript({Test-Path -PathType Container $_})] [string] - $OPAPath = $PSScriptRoot, + $OPAPath = (Join-Path -Path $PSScriptRoot -ChildPath "..\..\.."), + [Parameter(Mandatory = $false, ParameterSetName = 'Report')] + [ValidateNotNullOrEmpty()] + [ValidateSet($true, $false)] [boolean] $LogIn = $true, + [Parameter(Mandatory = $false)] + [switch] + $DisconnectOnExit, + + [Parameter(ParameterSetName = 'Report')] [switch] $Version, + [Parameter(Mandatory = $false, ParameterSetName = 'Report')] + [ValidateNotNullOrEmpty()] [string] - $OutPath = $PSScriptRoot - ) - process { - # The equivalent of ..\.. - $ParentPath = Split-Path $PSScriptRoot -Parent - $ParentPath = Split-Path $(Split-Path $ParentPath -Parent) -Parent - $ModuleVersion = $MyInvocation.MyCommand.ScriptBlock.Module.Version + $OutPath = '.', - if($Version) { - Write-Output("SCuBA Gear v$ModuleVersion") - return - } + [Parameter(Mandatory = $false, ParameterSetName = 'Report')] + [ValidateNotNullOrEmpty()] + [string] + $OutFolderName = "M365BaselineConformance", - # Create a folder to dump everything into - $Date = Get-Date - $DateStr = $Date.ToString("yyyy_MM_dd_HH_mm_ss") + [Parameter(Mandatory = $false, ParameterSetName = 'Report')] + [ValidateNotNullOrEmpty()] + [string] + $OutProviderFileName = "ProviderSettingsExport", - $FormattedTimeStamp = $DateStr + [Parameter(Mandatory = $false, ParameterSetName = 'Report')] + [ValidateNotNullOrEmpty()] + [string] + $OutRegoFileName = "TestResults", - $OutFolderPath = $OutPath - $FolderName = "M365BaselineConformance_$($FormattedTimeStamp)" - New-Item -Path $OutFolderPath -Name $($FolderName) -ItemType Directory | Out-Null - $OutFolderPath = Join-Path -Path $OutFolderPath -ChildPath $FolderName + [Parameter(Mandatory = $false, ParameterSetName = 'Report')] + [ValidateNotNullOrEmpty()] + [string] + $OutReportName = "BaselineReports" + ) + process { + $ParentPath = Split-Path $PSScriptRoot -Parent + $ScubaManifest = Import-PowerShellDataFile (Join-Path -Path $ParentPath -ChildPath 'ScubaGear.psd1' -Resolve) + $ModuleVersion = $ScubaManifest.ModuleVersion + if ($Version) { + Write-Output("SCuBA Gear v$ModuleVersion") + return + } - $ProductNames = $ProductNames | Sort-Object + if ($ProductNames -eq '*'){ + $ProductNames = "teams", "exo", "defender", "aad", "sharepoint", "onedrive", "powerplatform" + } - $ConnectionParams = @{ - 'LogIn' = $LogIn; - 'ProductNames' = $ProductNames; - 'Endpoint' = $Endpoint; - } + # The equivalent of ..\.. + $ParentPath = Split-Path $(Split-Path $ParentPath -Parent) -Parent - # If a PowerShell module is updated, the changes - # will not reflect until it is reimported into the runtime - Remove-Resources - Import-Resources # Imports Providers, RunRego, CreateReport + # Creates the output folder + $Date = Get-Date + $DateStr = $Date.ToString("yyyy_MM_dd_HH_mm_ss") + $FormattedTimeStamp = $DateStr - Invoke-Connection @ConnectionParams + $OutFolderPath = $OutPath + $FolderName = "$($OutFolderName)_$($FormattedTimeStamp)" + New-Item -Path $OutFolderPath -Name $($FolderName) -ItemType Directory | Out-Null + $OutFolderPath = Join-Path -Path $OutFolderPath -ChildPath $FolderName - $TenantDetails = Get-TenantDetails -ProductNames $ProductNames - $ProviderParams = @{ - 'ProductNames' = $ProductNames; - 'TenantDetails' = $TenantDetails; - 'OutFolderPath' = $OutFolderPath; + $ProductNames = $ProductNames | Sort-Object + + Remove-Resources + Import-Resources # Imports Providers, RunRego, CreateReport, Connection + + $ConnectionParams = @{ + 'LogIn' = $LogIn; + 'ProductNames' = $ProductNames; + 'M365Environment' = $M365Environment; + } + + $ProdAuthFailed = Invoke-Connection @ConnectionParams + if ($ProdAuthFailed.Count -gt 0) { + $Difference = Compare-Object $ProductNames -DifferenceObject $ProdAuthFailed -PassThru + if (-not $Difference) { + throw "All products were unable to establish a connection aborting execution" } - $RegoParams = @{ - 'ProductNames' = $ProductNames; - 'OPAPath' = $OPAPath; - 'ParentPath' = $ParentPath; - 'OutFolderPath' = $OutFolderPath; - } - $DisplayName = $TenantDetails | ConvertFrom-Json - $DisplayName = $DisplayName.DisplayName - $ReportParams = @{ - 'ProductNames' = $ProductNames; - 'DisplayName' = $DisplayName - 'OutFolderPath' = $OutFolderPath; + else { + $ProductNames = $Difference } + } + + $TenantDetails = Get-TenantDetail -ProductNames $ProductNames -M365Environment $M365Environment + $ProviderParams = @{ + 'ProductNames' = $ProductNames; + 'M365Environment' = $M365Environment; + 'TenantDetails' = $TenantDetails; + 'ModuleVersion' = $ModuleVersion; + 'OutFolderPath' = $OutFolderPath; + 'OutProviderFileName' = $OutProviderFileName; + } + $RegoParams = @{ + 'ProductNames' = $ProductNames; + 'OPAPath' = $OPAPath; + 'ParentPath' = $ParentPath; + 'OutFolderPath' = $OutFolderPath; + 'OutProviderFileName' = $OutProviderFileName; + 'OutRegoFileName' = $OutRegoFileName; + } + # Converted back from JSON String for PS Object use + $TenantDetails = $TenantDetails | ConvertFrom-Json + $ReportParams = @{ + 'ProductNames' = $ProductNames; + 'TenantDetails' = $TenantDetails; + 'ModuleVersion' = $ModuleVersion; + 'OutFolderPath' = $OutFolderPath; + 'OutProviderFileName' = $OutProviderFileName; + 'OutRegoFileName' = $OutRegoFileName; + 'OutReportName' = $OutReportName; + } + + try { Invoke-ProviderList @ProviderParams Invoke-RunRego @RegoParams Invoke-ReportCreation @ReportParams + } finally { + if ($DisconnectOnExit) { + if($VerbosePreference -eq "Continue") { + Disconnect-SCuBATenant -ProductNames $ProductNames -ErrorAction SilentlyContinue -Verbose + } + else { + Disconnect-SCuBATenant -ProductNames $ProductNames -ErrorAction SilentlyContinue + } + } } } +} $ArgToProd = @{ teams = "Teams"; @@ -113,6 +242,16 @@ $ArgToProd = @{ onedrive = "OneDrive"; } +$ProdToFullName = @{ + Teams = "Microsoft Teams"; + EXO = "Exchange Online"; + Defender = "Microsoft 365 Defender"; + AAD = "Azure Active Directory"; + PowerPlatform = "Microsoft Power Platform"; + SharePoint = "SharePoint Online"; + OneDrive = "OneDrive for Business"; +} + function Invoke-ProviderList { <# .Description @@ -127,46 +266,64 @@ function Invoke-ProviderList { [string[]] $ProductNames, + [Parameter(Mandatory=$true)] + [string] + $M365Environment, + [Parameter(Mandatory=$true)] [string] $TenantDetails, [Parameter(Mandatory=$true)] [string] - $OutFolderPath + $ModuleVersion, + + [Parameter(Mandatory=$true)] + [string] + $OutFolderPath, + + [Parameter(Mandatory=$true)] + [string] + $OutProviderFileName ) process { # yes the syntax has to be like this # fixing the spacing causes PowerShell interpreter errors $ProviderJSON = @" "@ - $ModuleVersion = $MyInvocation.MyCommand.ScriptBlock.Module.Version $N = 0 $Len = $ProductNames.Length foreach ($Product in $ProductNames) { $BaselineName = $ArgToProd[$Product] $N += 1 $Percent = $N*100/$Len - Write-Progress -Activity "Running the provider for each baseline" -Status "Running the $($BaselineName) Provider; $($n) of $($Len) Product settings extracted" -PercentComplete $Percent -Id 1 + $Status = "Running the $($BaselineName) Provider; $($N) of $($Len) Product settings extracted" + $ProgressParams = @{ + 'Activity' = "Running the provider for each baseline" + 'Status' = $Status; + 'PercentComplete' = $Percent; + 'Id' = 1; + } + Write-Progress @ProgressParams $RetVal = "" switch ($Product) { - "aad"{ + "aad" { $RetVal = Export-AADProvider | Select-Object -Last 1 } "exo" { $RetVal = Export-EXOProvider | Select-Object -Last 1 } "defender" { - $RetVal = Export-DefenderProvider | Select-Object -Last 1 + $RetVal = Export-DefenderProvider -M365Environment $M365Environment | Select-Object -Last 1 } - "powerplatform"{ + "powerplatform" { $RetVal = Export-PowerPlatformProvider | Select-Object -Last 1 } - "onedrive"{ - $RetVal = Export-OneDriveProvider | Select-Object -Last 1 + "onedrive" { + $RetVal = Export-OneDriveProvider -M365Environment $M365Environment | Select-Object -Last 1 } - "sharepoint"{ - $RetVal = Export-SharePointProvider | Select-Object -Last 1 + "sharepoint" { + $RetVal = Export-SharePointProvider -M365Environment $M365Environment | Select-Object -Last 1 } "teams" { $RetVal = Export-TeamsProvider | Select-Object -Last 1 @@ -178,12 +335,6 @@ function Invoke-ProviderList { $ProviderJSON += $RetVal } - # Clean up EXO - Defender conflicts - if($ProductNames -contains "defender") { - Disconnect-ExchangeOnline -Confirm:$false -InformationAction Ignore -ErrorAction SilentlyContinue | Out-Null - Disconnect-ExchangeOnline -Confirm:$false -InformationAction Ignore -ErrorAction SilentlyContinue | Out-Null - } - $ProviderJSON = $ProviderJSON.TrimEnd(",") $TimeZone = "" if ((Get-Date).IsDaylightSavingTime()) { @@ -202,7 +353,9 @@ function Invoke-ProviderList { $ProviderJSON } "@ - $FinalPath = Join-Path -Path $OutFolderPath -ChildPath "ProviderSettingsExport.json" + $BaselineSettingsExport = $BaselineSettingsExport.replace("\`"", "'") + $BaselineSettingsExport = $BaselineSettingsExport.replace("\", "") + $FinalPath = Join-Path -Path $OutFolderPath -ChildPath "$($OutProviderFileName).json" $BaselineSettingsExport | Set-Content -Path $FinalPath } } @@ -232,7 +385,15 @@ function Invoke-RunRego { [Parameter(Mandatory=$true)] [String] - $OutFolderPath + $OutFolderPath, + + [Parameter(Mandatory=$true)] + [String] + $OutProviderFileName, + + [Parameter(Mandatory=$true)] + [string] + $OutRegoFileName ) process { $TestResults = @() @@ -242,8 +403,16 @@ function Invoke-RunRego { $BaselineName = $ArgToProd[$Product] $N += 1 $Percent = $N*100/$Len - Write-Progress -Activity "Running the rego for each baseline" -Status "Running the $($BaselineName) Rego Verification; $($n) of $($Len) Rego verifications completed" -PercentComplete $Percent -Id 1 - $InputFile = Join-Path -Path $OutFolderPath "ProviderSettingsExport.json" + + $Status = "Running the $($BaselineName) Rego Verification; $($N) of $($Len) Rego verifications completed" + $ProgressParams = @{ + 'Activity' = "Running the rego for each baseline"; + 'Status' = $Status; + 'PercentComplete' = $Percent; + 'Id' = 1; + } + Write-Progress @ProgressParams + $InputFile = Join-Path -Path $OutFolderPath "$($OutProviderFileName).json" $RegoFile = Join-Path -Path $ParentPath -ChildPath "Rego" $RegoFile = Join-Path -Path $RegoFile -ChildPath "$($BaselineName)Config.rego" $params = @{ @@ -253,24 +422,25 @@ function Invoke-RunRego { 'OPAPath' = $OPAPath } $RetVal = Invoke-Rego @params - $TestResults += $RetVal + $TestResults += $RetVal } $TestResultsJson = $TestResults | ConvertTo-Json -Depth 5 - $FileName = Join-Path -path $OutFolderPath "TestResults.json" + $FileName = Join-Path -path $OutFolderPath "$($OutRegoFileName).json" $TestResultsJson | Set-Content -Path $FileName foreach ($Product in $TestResults) { foreach ($Test in $Product) { # ConvertTo-Csv struggles with the nested nature of the ActualValue - # field. Explicitly convert the ActualValues to json strings before + # and Commandlet fields. Explicitly convert these to json strings before # calling ConvertTo-Csv $Test.ActualValue = $Test.ActualValue | ConvertTo-Json -Depth 3 -Compress + $Test.Commandlet = $Test.Commandlet -Join ", " } } $TestResultsCsv = $TestResults | ConvertTo-Csv -NoTypeInformation - $CSVFileName = Join-Path -Path $OutFolderPath "TestResults.csv" + $CSVFileName = Join-Path -Path $OutFolderPath "$($OutRegoFileName).csv" $TestResultsCsv | Set-Content -Path $CSVFileName } } @@ -299,10 +469,10 @@ function Pluralize { ) process { if ($Count -gt 1) { - return $PluralNoun + $PluralNoun } else { - return $SingularNoun + $SingularNoun } } } @@ -323,19 +493,38 @@ function Invoke-ReportCreation { [string[]] $ProductNames, + [Parameter(Mandatory=$true)] + [object] + $TenantDetails, + [Parameter(Mandatory=$true)] [string] - $DisplayName, + $ModuleVersion, [Parameter(Mandatory=$true)] - [String] - $OutFolderPath + [string] + $OutFolderPath, + + [Parameter(Mandatory=$true)] + [string] + $OutProviderFileName, + + [Parameter(Mandatory=$true)] + [string] + $OutRegoFileName, + + [Parameter(Mandatory=$true)] + [string] + $OutReportName, + + [Parameter(Mandatory = $false)] + [boolean] + $Quiet = $false ) process { $N = 0 $Len = $ProductNames.Length $Fragment = @() - $ModuleVersion = $MyInvocation.MyCommand.Module.Version $IndividualReportFolderName = "IndividualReports" $IndividualReportPath = Join-Path -Path $OutFolderPath -ChildPath $IndividualReportFolderName @@ -345,21 +534,18 @@ function Invoke-ReportCreation { $Logo = Join-Path -Path $ReporterPath -ChildPath "cisa_logo.png" Copy-Item -Path $Logo -Destination $IndividualReportPath -Force - $ProdToFullName = @{ - Teams = "Microsoft Teams"; - EXO = "Exchange Online"; - Defender = "Microsoft 365 Defender"; - AAD = "Azure Active Directory"; - PowerPlatform = "Microsoft Power Platform"; - SharePoint = "SharePoint Online"; - OneDrive = "OneDrive for Business"; - } - foreach ($Product in $ProductNames) { $BaselineName = $ArgToProd[$Product] $N += 1 $Percent = $N*100/$Len - Write-Progress -Activity "Creating the reports for each baseline" -Status "Running the $($BaselineName) Report creation; $($n) of $($Len) Baselines Reports created" -PercentComplete $Percent -Id 1 + $Status = "Running the $($BaselineName) Report creation; $($N) of $($Len) Baselines Reports created"; + $ProgressParams = @{ + 'Activity' = "Creating the reports for each baseline" + 'Status' = $Status; + 'PercentComplete' = $Percent; + 'Id' = 1; + } + Write-Progress @ProgressParams $FullName = $ProdToFullName[$BaselineName] @@ -368,6 +554,8 @@ function Invoke-ReportCreation { 'FullName' = $FullName; 'IndividualReportPath' = $IndividualReportPath; 'OutPath' = $OutFolderPath; + 'OutProviderFileName' = $OutProviderFileName; + 'OutRegoFileName' = $OutRegoFileName; } $Report = New-Report @CreateReportParams @@ -379,6 +567,7 @@ function Invoke-ReportCreation { $WarningsSummary = "
" $FailuresSummary = "
" $ManualSummary = "
" + $ErrorSummary = "
" if ($Report.Warnings -gt 0) { $Noun = Pluralize -SingularNoun "warning" -PluralNoun "warnings" -Count $Report.Warnings @@ -395,18 +584,29 @@ function Invoke-ReportCreation { $ManualSummary = "
$($Report.Manual) manual $($Noun) needed
" } + if ($Report.Errors -gt 0) { + $Noun = Pluralize -SingularNoun "check" -PluralNoun "errors" -Count $Report.Manual + $ErrorSummary = "
$($Report.Errors) PowerShell $($Noun)
" + } + $Fragment += [pscustomobject]@{ "Baseline Conformance Reports" = $Link; - "Details" = "$($PassesSummary) $($WarningsSummary) $($FailuresSummary) $($ManualSummary)" + "Details" = "$($PassesSummary) $($WarningsSummary) $($FailuresSummary) $($ManualSummary) $($ErrorSummary)" } } - + $TenantMetaData += [pscustomobject]@{ + "Tenant Display Name" = $TenantDetails.DisplayName; + "Tenant Domain Name" = $TenantDetails.DomainName + "Tenant ID" = $TenantDetails.TenantId; + "Report Date" = $Report.Date; + } + $TenantMetaData = $TenantMetaData | ConvertTo-Html -Fragment + $TenantMetaData = $TenantMetaData -replace '^(.*?)
','
' $Fragment = $Fragment | ConvertTo-Html -Fragment $ReportHTML = Get-Content $(Join-Path -Path $ReporterPath -ChildPath "ParentReportTemplate.html") - $ReportHTML = $ReportHTML.Replace("{TENANT_NAME}", $DisplayName) + $ReportHTML = $ReportHTML.Replace("{TENANT_DETAILS}", $TenantMetaData) $ReportHTML = $ReportHTML.Replace("{TABLES}", $Fragment) - $ReportHTML = $ReportHTML.Replace("{REPORT_TIME}", $Report.Date) $ReportHTML = $ReportHTML.Replace("{MODULE_VERSION}", "v$ModuleVersion") $MainCSS = Get-Content $(Join-Path -Path $ReporterPath -ChildPath "main.css") @@ -416,13 +616,15 @@ function Invoke-ReportCreation { $ReportHTML = $ReportHTML.Replace("{PARENT_CSS}", "") Add-Type -AssemblyName System.Web - $ReportFileName = Join-Path -Path $OutFolderPath "BaselineReports.html" + $ReportFileName = Join-Path -Path $OutFolderPath "$($OutReportName).html" [System.Web.HttpUtility]::HtmlDecode($ReportHTML) | Out-File $ReportFileName - Invoke-Item $ReportFileName + if ($Quiet -eq $False) { + Invoke-Item $ReportFileName + } } } -function Get-TenantDetails { +function Get-TenantDetail { <# .Description This function gets the details of the M365 Tenant using @@ -433,34 +635,45 @@ function Get-TenantDetails { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] + [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", "onedrive", IgnoreCase = $false)] [string[]] - $ProductNames + $ProductNames, + + [Parameter(Mandatory = $true)] + [ValidateSet("commercial", "gcc", "gcchigh", "dod", IgnoreCase = $false)] + [string] + $M365Environment ) # organized by best tenant details information - if ($ProductNames.Contains("teams")) { - Get-TeamsTenantDetail + if ($ProductNames.Contains("aad")) { + Get-AADTenantDetail } - elseif ($ProductNames.Contains("aad")) { + elseif ($ProductNames.Contains("sharepoint")) { Get-AADTenantDetail } - elseif ($ProductNames.Contains("exo")) { - Get-EXOTenantDetail + elseif ($ProductNames.Contains("onedrive")) { + Get-AADTenantDetail } - elseif ($ProductNames.Contains("defender")) { - Get-DefenderTenantDetail + elseif ($ProductNames.Contains("teams")) { + Get-TeamsTenantDetail -M365Environment $M365Environment } elseif ($ProductNames.Contains("powerplatform")) { - Get-PowerPlatformTenantDetail + Get-PowerPlatformTenantDetail -M365Environment $M365Environment } - elseif ($ProductNames.Contains("sharepoint")) { - Get-AADTenantDetail + elseif ($ProductNames.Contains("exo")) { + Get-EXOTenantDetail -M365Environment $M365Environment } - elseif ($ProductNames.Contains("onedrive")) { - Get-AADTenantDetail + elseif ($ProductNames.Contains("defender")) { + Get-EXOTenantDetail -M365Environment $M365Environment } else { - $TenantInfo = @{"DisplayName"="Undefined Name";} + $TenantInfo = @{ + "DisplayName" = "Orchestrator Error retrieving Display name"; + "DomainName" = "Orchestrator Error retrieving Domain name"; + "TenantId" = "Orchestrator Error retrieving Tenant ID"; + "AdditionalData" = "Orchestrator Error retrieving additional data"; + } $TenantInfo = $TenantInfo | ConvertTo-Json -Depth 3 $TenantInfo } @@ -486,14 +699,18 @@ function Invoke-Connection { [string[]] $ProductNames, + [ValidateSet("commercial", "gcc", "gcchigh", "dod")] [string] - $Endpoint + $M365Environment = "commercial" ) + + # Increase PowerShell Maximum Function Count to support version 5.1 limitation + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'MaximumFunctionCount')] + $MaximumFunctionCount = 32000 + if ($LogIn) { - $ConnectionPath = Join-Path -Path $PSScriptRoot -ChildPath "Connection" - Remove-Module "Connection" -ErrorAction "SilentlyContinue" - Import-Module $ConnectionPath - Connect-Tenant -ProductNames $ProductNames -Endpoint $Endpoint + $AnyFailedAuth = Connect-Tenant -ProductNames $ProductNames -M365Environment $M365Environment + $AnyFailedAuth } } @@ -505,14 +722,14 @@ function Import-Resources { .Functionality Internal #> + [CmdletBinding()] $ProvidersPath = Join-Path -Path $PSScriptRoot ` -ChildPath "Providers" ` -Resolve $ProviderResources = Get-ChildItem $ProvidersPath -Recurse | Where-Object { $_.Name -like 'Export*.psm1' } if (!$ProviderResources) { - Write-Error "Provider files were not found, aborting" - break + throw "Provider files were not found, aborting this run" } foreach ($Provider in $ProviderResources.Name) { @@ -520,8 +737,10 @@ function Import-Resources { $ModulePath = Join-Path -Path $ProvidersPath -ChildPath $Provider Import-Module $ModulePath } + $ConnectionPath = Join-Path -Path $PSScriptRoot -ChildPath "Connection" $RegoPath = Join-Path -Path $PSScriptRoot -ChildPath "RunRego" $ReporterPath = Join-Path -Path $PSScriptRoot -ChildPath "CreateReport" + Import-Module $ConnectionPath Import-Module $RegoPath Import-Module $ReporterPath } @@ -534,6 +753,7 @@ function Remove-Resources { .Functionality Internal #> + [CmdletBinding()] $Providers = @("ExportPowerPlatform", "ExportEXOProvider", "ExportAADProvider", "ExportDefenderProvider", "ExportTeamsProvider", "ExportSharePointProvider", "ExportOneDriveProvider") foreach ($Provider in $Providers) { @@ -547,90 +767,227 @@ function Remove-Resources { function Invoke-RunCached { <# + .SYNOPSIS + Specially execute the SCuBAGear tool security baselines for specified M365 products. + Can be executed on static provider JSON. .Description - This is the function for Rego testing. Sometimes you don't want to pull the provider - JSON every single time. + This is the function for running the tool provider JSON that has already been extracted. This functions comes with the extra ExportProvider parameter to omit exporting the provider - if set to $false + if set to $false. The rego will be run on a static provider JSON in the specified OutPath. + <# + .Parameter ExportProvider + This parameter will when set to $true export the provider and act like Invoke-Scuba. + When set to $false will instead omit authentication plus pulling the provider and will + instead look in OutPath and run just the Rego verification and Report creation. + .Parameter ProductNames + A list of one or more M365 shortened product names that the tool will assess when it is executed. Acceptable product name values are listed below. + To assess Azure Active Directory you would enter the value aad. + To assess Exchange Online you would enter exo and so forth. + - Azure Active Directory: aad + - Defender for Office 365: defender + - Exchange Online: exo + - OneDrive: onedrive + - MS Power Platform: powerplatform + - SharePoint Online: sharepoint + - MS Teams: teams. + Use '*' to run all baselines. + .Parameter M365Environment + This parameter is used to authenticate to the different commercial/government environments. + Valid values include "commercial", "gcc", "gcchigh", or "dod". + For M365 tenants with E3/E5 licenses enter the value **"commercial"**. + For M365 Government Commercial Cloud tenants with G3/G5 licenses enter the value **"gcc"**. + For M365 Government Commercial Cloud High tenants enter the value **"gcchigh"**. + For M365 Department of Defense tenants enter the value **"dod"**. + Default is 'commercial'. + .Parameter OPAPath + The folder location of the OPA Rego executable file. + The OPA Rego executable embedded with this project is located in the project's root folder. + If you want to execute the tool using a version of OPA Rego located in another folder, + then customize the variable value with the full path to the alternative OPA Rego exe file. + .Parameter LogIn + A `$true` or `$false` variable that if set to `$true` + will prompt you to provide credentials if you want to establish a connection + to the specified M365 products in the **$ProductNames** variable. + For most use cases, leave this variable to be `$true`. + A connection is established in the current PowerShell terminal session with the first authentication. + If you want to run another verification in the same PowerShell session simply set + this variable to be `$false` to bypass the reauthenticating in the same session. Default is $true. + .Parameter Version + Will output the current ScubaGear version to the terminal without running this cmdlet. + .Parameter OutPath + The folder path where both the output JSON and the HTML report will be created. + The folder will be created if it does not exist. Defaults to current directory. + .Parameter OutFolderName + The name of the folder in OutPath where both the output JSON and the HTML report will be created. + Defaults to "M365BaselineConformance". The client's local timestamp will be appended. + .Parameter OutProviderFileName + The name of the Provider output JSON created in the folder created in OutPath. + Defaults to "ProviderSettingsExport". + .Parameter OutRegoFileName + The name of the Rego output JSON and CSV created in the folder created in OutPath. + Defaults to "TestResults". + .Parameter OutReportName + The name of the main html file page created in the folder created in OutPath. + Defaults to "BaselineReports". + .Example + Invoke-RunCached + Run an assessment against by default a commercial M365 Tenant against the + Azure Active Directory, Exchange Online, Microsoft Defender, One Drive, SharePoint Online, and Microsoft Teams + security baselines. The output will stored in the current directory in a folder called M365BaselineConformaance_*. + .Example + Invoke-RunCached -Version + This example returns the version of SCuBAGear. + .Example + Invoke-RunCached -ProductNames aad, defender -OPAPath . -OutPath . + The example will run the tool against the Azure Active Directory, and Defender security + baselines. + .Example + Invoke-RunCached -ProductNames * -M365Environment dod -OPAPath . -OutPath . + This example will run the tool against all available security baselines with the + 'dod' teams endpoint. .Functionality Public #> [CmdletBinding()] param ( - [Parameter(Mandatory=$true)] - [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", "onedrive", IgnoreCase = $false)] + [Parameter(Mandatory = $false, ParameterSetName = 'Report')] + [ValidateNotNullOrEmpty()] + [boolean] + $ExportProvider = $true, + + [Parameter(Mandatory = $false, ParameterSetName = 'Report')] + [ValidateNotNullOrEmpty()] + [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", "onedrive", '*', IgnoreCase = $false)] [string[]] - $ProductNames, + $ProductNames = '*', + [Parameter(Mandatory = $false, ParameterSetName = 'Report')] + [ValidateSet("commercial", "gcc", "gcchigh", "dod")] [string] - $Endpoint = "", + $M365Environment = "commercial", - # The path to the OPA executable. Defaults to this directory. - [Parameter(Mandatory=$true)] + [Parameter(Mandatory = $false, ParameterSetName = 'Report')] + [ValidateScript({Test-Path -PathType Container $_})] [string] - $OPAPath = $PSScriptRoot, + $OPAPath = (Join-Path -Path $PSScriptRoot -ChildPath "..\..\.."), + [Parameter(Mandatory = $false, ParameterSetName = 'Report')] + [ValidateNotNullOrEmpty()] + [ValidateSet($true, $false)] [boolean] $LogIn = $true, - # true to export the provider - # false to not export - [Parameter(Mandatory=$true)] - [boolean] - $ExportProvider, + [Parameter(ParameterSetName = 'Report')] + [switch] + $Version, - # The destination folder for the output. + [Parameter(Mandatory = $false, ParameterSetName = 'Report')] + [ValidateNotNullOrEmpty()] [string] - $OutPath = $PSScriptRoot + $OutPath = '.', + + [Parameter(Mandatory = $false, ParameterSetName = 'Report')] + [ValidateNotNullOrEmpty()] + [string] + $OutProviderFileName = "ProviderSettingsExport", + + [Parameter(Mandatory = $false, ParameterSetName = 'Report')] + [ValidateNotNullOrEmpty()] + [string] + $OutRegoFileName = "TestResults", + + [Parameter(Mandatory = $false, ParameterSetName = 'Report')] + [ValidateNotNullOrEmpty()] + [string] + $OutReportName = "BaselineReports", + + [Parameter(Mandatory = $false, ParameterSetName = 'Report')] + [ValidateNotNullOrEmpty()] + [ValidateSet($true, $false)] + [boolean] + $Quiet = $false ) process { - # The equivalent of ..\.. $ParentPath = Split-Path $PSScriptRoot -Parent + $ScubaManifest = Import-PowerShellDataFile (Join-Path -Path $ParentPath -ChildPath 'ScubaGear.psd1' -Resolve) + $ModuleVersion = $ScubaManifest.ModuleVersion + + if ($Version) { + Write-Output("SCuBA Gear v$ModuleVersion") + return + } + + if ($ProductNames -eq '*'){ + $ProductNames = "teams", "exo", "defender", "aad", "sharepoint", "onedrive" + } + + # The equivalent of ..\.. $ParentPath = Split-Path $(Split-Path $ParentPath -Parent) -Parent + # Create outpath if $Outpath does not exist + if(-not (Test-Path -PathType "container" $OutPath)) + { + New-Item -ItemType "Directory" -Path $OutPath | Out-Null + } $OutFolderPath = $OutPath $ProductNames = $ProductNames | Sort-Object + Remove-Resources + Import-Resources # Imports Providers, RunRego, CreateReport, Connection + # Authenticate $ConnectionParams = @{ 'LogIn' = $LogIn; 'ProductNames' = $ProductNames; - 'Endpoint' = $Endpoint; + 'M365Environment' = $M365Environment; } - #Rego Testing + # Rego Testing failsafe $TenantDetails = @{"DisplayName"="Rego Testing";} $TenantDetails = $TenantDetails | ConvertTo-Json -Depth 3 - - $ProviderParams = @{ - 'ProductNames' = $ProductNames; - 'TenantDetails' = $TenantDetails; - 'OutFolderPath' = $OutFolderPath; + if ($ExportProvider) { + $ProdAuthFailed = Invoke-Connection @ConnectionParams + if ($ProdAuthFailed.Count -gt 0) { + $Difference = Compare-Object $ProductNames -DifferenceObject $ProdAuthFailed -PassThru + if (-not $Difference) { + throw "All products were unable to establish a connection aborting execution" + } + else { + $ProductNames = $Difference + } + } + $TenantDetails = Get-TenantDetail -ProductNames $ProductNames -M365Environment $M365Environment + $ProviderParams = @{ + 'ProductNames' = $ProductNames; + 'M365Environment' = $M365Environment; + 'TenantDetails' = $TenantDetails; + 'ModuleVersion' = $ModuleVersion; + 'OutFolderPath' = $OutFolderPath; + 'OutProviderFileName' = $OutProviderFileName; + } + Invoke-ProviderList @ProviderParams } + $FileName = Join-Path -Path $OutPath -ChildPath "$($OutProviderFileName).json" + $SettingsExport = Get-Content $FileName | ConvertFrom-Json + $TenantDetails = $SettingsExport.tenant_details $RegoParams = @{ 'ProductNames' = $ProductNames; 'OPAPath' = $OPAPath; 'ParentPath' = $ParentPath; 'OutFolderPath' = $OutFolderPath; + 'OutProviderFileName' = $OutProviderFileName; + 'OutRegoFileName' = $OutRegoFileName; } - - $DisplayName = $TenantDetails | ConvertFrom-Json - $DisplayName = $DisplayName.DisplayName $ReportParams = @{ 'ProductNames' = $ProductNames; - 'DisplayName' = $DisplayName + 'TenantDetails' = $TenantDetails; + 'ModuleVersion' = $ModuleVersion; 'OutFolderPath' = $OutFolderPath; - } - - # If a PowerShell module is updated, the changes - # will not reflect until it is reimported into the runtime - Remove-Resources - Import-Resources # Imports Providers, RunRego, CreateReport - - if ($ExportProvider) { - Invoke-Connection @ConnectionParams - Invoke-ProviderList @ProviderParams + 'OutProviderFileName' = $OutProviderFileName; + 'OutRegoFileName' = $OutRegoFileName; + 'OutReportName' = $OutReportName; + 'Quiet' = $Quiet; } Invoke-RunRego @RegoParams Invoke-ReportCreation @ReportParams @@ -640,4 +997,4 @@ function Invoke-RunCached { Export-ModuleMember -Function @( 'Invoke-SCuBA', 'Invoke-RunCached' -) +) \ No newline at end of file diff --git a/PowerShell/ScubaGear/Modules/Providers/ExportAADProvider.psm1 b/PowerShell/ScubaGear/Modules/Providers/ExportAADProvider.psm1 index a82096a4f7..5fb900b32c 100644 --- a/PowerShell/ScubaGear/Modules/Providers/ExportAADProvider.psm1 +++ b/PowerShell/ScubaGear/Modules/Providers/ExportAADProvider.psm1 @@ -78,6 +78,7 @@ function Export-AADProvider { $json } + function Get-AADTenantDetail { <# .Description @@ -85,10 +86,32 @@ function Get-AADTenantDetail { .Functionality Internal #> - $TenantInfo = @{} - $TenantInfo.DisplayName = $(Get-MgOrganization).DisplayName - $TenantInfo = $TenantInfo | ConvertTo-Json -Depth 4 - $TenantInfo + try { + $OrgInfo = Get-MgOrganization -ErrorAction "Stop" + $InitialDomain = $OrgInfo.VerifiedDomains | Where-Object {$_.isInitial} + if (-not $InitialDomain) { + $InitialDomain = "AAD: Domain Unretrievable" + } + $AADTenantInfo = @{ + "DisplayName" = $OrgInfo.DisplayName; + "DomainName" = $InitialDomain.Name; + "TenantId" = $OrgInfo.Id + "AADAdditionalData" = $OrgInfo; + } + $AADTenantInfo = ConvertTo-Json @($AADTenantInfo) -Depth 4 + $AADTenantInfo + } + catch { + Write-Warning "Error retrieving Tenant details using Get-AADTenantDetail $($_)" + $AADTenantInfo = @{ + "DisplayName" = "Error retrieving Display name"; + "DomainName" = "Error retrieving Domain name"; + "TenantId" = "Error retrieving Tenant ID"; + "AADAdditionalData" = "Error retrieving additional data"; + } + $AADTenantInfo = ConvertTo-Json @($AADTenantInfo) -Depth 4 + $AADTenantInfo + } } function Get-PrivilegedUser { diff --git a/PowerShell/ScubaGear/Modules/Providers/ExportDefenderProvider.psm1 b/PowerShell/ScubaGear/Modules/Providers/ExportDefenderProvider.psm1 index f66e772eae..0f04ffa876 100644 --- a/PowerShell/ScubaGear/Modules/Providers/ExportDefenderProvider.psm1 +++ b/PowerShell/ScubaGear/Modules/Providers/ExportDefenderProvider.psm1 @@ -6,64 +6,141 @@ function Export-DefenderProvider { .Functionality Internal #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateSet("commercial", "gcc", "gcchigh", "dod", IgnoreCase = $false)] + [string] + $M365Environment + ) + $ParentPath = Split-Path $PSScriptRoot -Parent + $ConnectionFolderPath = Join-Path -Path $ParentPath -ChildPath "Connection" + Import-Module (Join-Path -Path $ConnectionFolderPath -ChildPath "ConnectHelpers.psm1") + + $HelperFolderPath = Join-Path -Path $PSScriptRoot -ChildPath "ProviderHelpers" + Import-Module (Join-Path -Path $HelperFolderPath -ChildPath "CommandTracker.psm1") + $Tracker = Get-CommandTracker + + # Manually importing the module name here to bypass cmdlet name conflicts + # There are conflicting PowerShell Cmdlet names in EXO and Power Platform + Import-Module ExchangeOnlineManagement # Sign in for the Defender Provider if not connected - $ExchangeConnected = Get-OrganizationConfig -ErrorAction SilentlyContinue + $ExchangeConnected = Get-Command Get-OrganizationConfig -ErrorAction SilentlyContinue if(-not $ExchangeConnected) { - Connect-ExchangeOnline -ShowBanner:$false | Out-Null + try { + Connect-EXOHelper -M365Environment $M365Environment + } + catch { + Write-Error "Error connecting to ExchangeOnline. $($_)" + } } - Import-Module ExchangeOnlineManagement # Regular Exchange i.e non IPPSSession cmdlets - $AdminAuditLogConfig = Get-AdminAuditLogConfig | ConvertTo-Json - $ProtectionPolicyRule = ConvertTo-Json @(Get-EOPProtectionPolicyRule) - $MalwareFilterPolicy = ConvertTo-Json @(Get-MalwareFilterPolicy) - $AntiPhishPolicy = ConvertTo-Json @(Get-AntiPhishPolicy) - $HostedContentFilterPolicy = ConvertTo-Json @(Get-HostedContentFilterPolicy) - $AllDomains = Get-AcceptedDomain | ConvertTo-Json + $AdminAuditLogConfig = ConvertTo-Json @($Tracker.TryCommand("Get-AdminAuditLogConfig")) + $ProtectionPolicyRule = ConvertTo-Json @($Tracker.TryCommand("Get-EOPProtectionPolicyRule")) + $MalwareFilterPolicy = ConvertTo-Json @($Tracker.TryCommand("Get-MalwareFilterPolicy")) + $AntiPhishPolicy = ConvertTo-Json @($Tracker.TryCommand("Get-AntiPhishPolicy")) + $HostedContentFilterPolicy = ConvertTo-Json @($Tracker.TryCommand("Get-HostedContentFilterPolicy")) + $AllDomains = ConvertTo-Json @($Tracker.TryCommand("Get-AcceptedDomain")) # Test if Defender specific commands are available. If the tenant does # not have a defender license (plan 1 or plan 2), the following # commandlets will fail with "The term [Cmdlet name] is not recognized # as the name of a cmdlet, function, script file, or operable program," # so we can test for this using Get-Command. - if (Get-Command Get-SafeAttachmentPolicy -errorAction SilentlyContinue) { - $SafeAttachmentPolicy = ConvertTo-Json @(Get-SafeAttachmentPolicy) - $SafeAttachmentRule = ConvertTo-Json @(Get-SafeAttachmentRule) - $SafeLinksPolicy = ConvertTo-Json @(Get-SafeLinksPolicy) - $SafeLinksRule = ConvertTo-Json @(Get-SafeLinksRule) - $ATPPolicy = ConvertTo-Json @(Get-AtpPolicyForO365) + if (Get-Command Get-SafeAttachmentPolicy -ErrorAction SilentlyContinue) { + $SafeAttachmentPolicy = ConvertTo-Json @($Tracker.TryCommand("Get-SafeAttachmentPolicy")) + $SafeAttachmentRule = ConvertTo-Json @($Tracker.TryCommand("Get-SafeAttachmentRule")) + $SafeLinksPolicy = ConvertTo-Json @($Tracker.TryCommand("Get-SafeLinksPolicy")) + $SafeLinksRule = ConvertTo-Json @($Tracker.TryCommand("Get-SafeLinksRule")) + $ATPPolicy = ConvertTo-Json @($Tracker.TryCommand("Get-AtpPolicyForO365")) $DefenderLicense = ConvertTo-Json $true } else { # The tenant can't make use of the defender commands + Write-Warning "Defender license not available in tenant. Omitting the following commands: Get-SafeAttachmentPolicy, Get-SafeAttachmentRule, Get-SafeLinksPolicy, Get-SafeLinksRule, and Get-AtpPolicyForO365." $SafeAttachmentPolicy = ConvertTo-Json @() $SafeAttachmentRule = ConvertTo-Json @() $SafeLinksPolicy = ConvertTo-Json @() $SafeLinksRule = ConvertTo-Json @() $ATPPolicy = ConvertTo-Json @() $DefenderLicense = ConvertTo-Json $false - } - $AllDomains = ConvertTo-Json @(Get-AcceptedDomain) + # While it is counter-intuitive to add these to SuccessfulCommands + # and UnSuccessfulCommands, this is a unique error case that is + # handled within the Rego. + $Tracker.AddSuccessfulCommand("Get-SafeAttachmentPolicy") + $Tracker.AddSuccessfulCommand("Get-SafeAttachmentRule") + $Tracker.AddSuccessfulCommand("Get-SafeLinksPolicy") + $Tracker.AddSuccessfulCommand("Get-SafeLinksRule") + $Tracker.AddSuccessfulCommand("Get-AtpPolicyForO365") + + $Tracker.AddUnSuccessfulCommand("Get-SafeAttachmentPolicy") + $Tracker.AddUnSuccessfulCommand("Get-SafeAttachmentRule") + $Tracker.AddUnSuccessfulCommand("Get-SafeLinksPolicy") + $Tracker.AddUnSuccessfulCommand("Get-SafeLinksRule") + $Tracker.AddUnSuccessfulCommand("Get-AtpPolicyForO365") + } # Connect to Security & Compliance - Connect-IPPSSession | Out-Null + $IPPSConnected = $false + $IPPSParams = @{ + 'ErrorAction' = 'Stop'; + } + try { + switch ($M365Environment) { + {($_ -eq "commercial") -or ($_ -eq "gcc")} { + $IPPSParams = @{'ErrorAction' = 'Stop';} # sanity check + } + "gcchigh" { + $IPPSParams = $IPPSParams + @{'ConnectionUri' = "https://outlook.office365.us/powershell-liveID";} + } + "dod" { + $IPPSParams = $IPPSParams + @{'ConnectionUri' = "https://webmail.apps.mil/powershell-liveID";} + } + default { + throw -Message "Unsupported or invalid M365Environment argument" + } + } + Connect-IPPSSession @IPPSParams | Out-Null + $IPPSConnected = $true + } + catch { + Write-Error "Error running Connect-IPPSSession. $($_)" + Write-Warning "Omitting the following commands: Get-DlpCompliancePolicy, Get-DlpComplianceRule, and Get-ProtectionAlert." + $Tracker.AddUnSuccessfulCommand("Get-DlpCompliancePolicy") + $Tracker.AddUnSuccessfulCommand("Get-DlpComplianceRule") + $Tracker.AddUnSuccessfulCommand("Get-ProtectionAlert") + } + if ($IPPSConnected) { + $DLPCompliancePolicy = ConvertTo-Json @($Tracker.TryCommand("Get-DlpCompliancePolicy")) + $ProtectionAlert = ConvertTo-Json @($Tracker.TryCommand("Get-ProtectionAlert")) + $DLPComplianceRules = @($Tracker.TryCommand("Get-DlpComplianceRule")) + + # Powershell is inconsistent with how it saves lists to json. + # This loop ensures that the format of ContentContainsSensitiveInformation + # will *always* be a list. - $DLPCompliancePolicy = ConvertTo-Json @(Get-DlpCompliancePolicy) - $DLPComplianceRules = @(Get-DlpComplianceRule) - $ProtectionAlert = Get-ProtectionAlert | ConvertTo-Json + foreach($Rule in $DLPComplianceRules) { + if ($Rule.Count -gt 0) { + $Rule.ContentContainsSensitiveInformation = @($Rule.ContentContainsSensitiveInformation) + } + } - # Powershell is inconsistent with how they save lists to json. - # This loop ensures that the format of ContentContainsSensitiveInformation - # will *always* be a list. - foreach($Rule in $DLPComplianceRules) { - $Rule.ContentContainsSensitiveInformation = @($Rule.ContentContainsSensitiveInformation) + # We need to specify the depth because the data contains some + # nested tables. + $DLPComplianceRules = ConvertTo-Json -Depth 3 $DLPComplianceRules + } + else { + $DLPCompliancePolicy = ConvertTo-Json @() + $DLPComplianceRules = ConvertTo-Json @() + $ProtectionAlert = ConvertTo-Json @() + $DLPComplianceRules = ConvertTo-Json @() } - # We need to specify the depth because the data contains some - # nested tables. - $DLPComplianceRules = ConvertTo-Json -Depth 3 $DLPComplianceRules + $SuccessfulCommands = ConvertTo-Json @($Tracker.GetSuccessfulCommands()) + $UnSuccessfulCommands = ConvertTo-Json @($Tracker.GetUnSuccessfulCommands()) # Note the spacing and the last comma in the json is important $json = @" @@ -82,6 +159,8 @@ function Export-DefenderProvider { "safe_links_rules": $SafeLinksRule, "atp_policy_for_o365": $ATPPolicy, "defender_license": $DefenderLicense, + "defender_successful_commands": $SuccessfulCommands, + "defender_unsuccessful_commands": $UnSuccessfulCommands, "@ # We need to remove the backslash characters from the @@ -90,17 +169,3 @@ function Export-DefenderProvider { $json = $json.replace("\", "") $json } - -function Get-DefenderTenantDetail { - <# - .Description - Gets the tenant details using the AAD PowerShell Module - .Functionality - Internal - #> - Import-Module ExchangeOnlineManagement - $Config = Get-OrganizationConfig - $TenantInfo = @{"DisplayName"=$Config.Name;} - $TenantInfo = $TenantInfo | ConvertTo-Json -Depth 4 - $TenantInfo -} diff --git a/PowerShell/ScubaGear/Modules/Providers/ExportEXOProvider.psm1 b/PowerShell/ScubaGear/Modules/Providers/ExportEXOProvider.psm1 index 6fda0ba481..eddf6e5490 100644 --- a/PowerShell/ScubaGear/Modules/Providers/ExportEXOProvider.psm1 +++ b/PowerShell/ScubaGear/Modules/Providers/ExportEXOProvider.psm1 @@ -7,36 +7,176 @@ function Export-EXOProvider { Internal #> + # Manually importing the module name here to bypass cmdlet name conflicts + # There are conflicting PowerShell Cmdlet names in EXO and Power Platform Import-Module ExchangeOnlineManagement + + Import-Module $PSScriptRoot/ProviderHelpers/CommandTracker.psm1 + $Tracker = Get-CommandTracker <# 2.1 #> - $RemoteDomains = @(Get-RemoteDomain) - foreach ($d in $RemoteDomains) { - # Need to explicitly convert these values to strings, otherwise - # these fields contain values Rego can't parse. - $d.WhenChanged = $d.WhenChanged.ToString() - $d.WhenCreated = $d.WhenCreated.ToString() - $d.WhenChangedUTC = $d.WhenChangedUTC.ToString() - $d.WhenCreatedUTC = $d.WhenCreatedUTC.ToString() - } + $RemoteDomains = ConvertTo-Json @($Tracker.TryCommand("Get-RemoteDomain")) - $RemoteDomains = ConvertTo-Json $RemoteDomains <# 2.2 SPF #> + $domains = $Tracker.TryCommand("Get-AcceptedDomain") + $SPFRecords = ConvertTo-Json @($Tracker.TryCommand("Get-ScubaSpfRecords", @{"Domains"=$domains})) - $SPFRecords = @() + <# + 2.3 DKIM + #> + $DKIMConfig = ConvertTo-Json @($Tracker.TryCommand("Get-DkimSigningConfig")) + $DKIMRecords = ConvertTo-Json @($Tracker.TryCommand("Get-ScubaDkimRecords", @{"Domains"=$domains})) - $domains = Get-AcceptedDomain + <# + 2.4 DMARC + #> + $DMARCRecords = ConvertTo-Json @($Tracker.TryCommand("Get-ScubaDmarcRecords", @{"Domains"=$domains})) - foreach ($d in $domains) { + <# + 2.5 + #> + + $TransportConfig = ConvertTo-Json @($Tracker.TryCommand("Get-TransportConfig")) + + <# + 2.6 + #> + $SharingPolicy = ConvertTo-Json @($Tracker.TryCommand("Get-SharingPolicy")) + + <# + 2.7 + #> + + $TransportRules = ConvertTo-Json @($Tracker.TryCommand("Get-TransportRule")) + + <# + 2.12 + #> + + $ConnectionFilter = ConvertTo-Json @($Tracker.TryCommand("Get-HostedConnectionFilterPolicy")) + + <# + 2.13 + #> + + $Config = ConvertTo-Json @($Tracker.TryCommand("Get-OrganizationConfig")) + + + $SuccessfulCommands = ConvertTo-Json @($Tracker.GetSuccessfulCommands()) + $UnSuccessfulCommands = ConvertTo-Json @($Tracker.GetUnSuccessfulCommands()) + + <# + Save output + #> + $json = @" + "remote_domains": $RemoteDomains, + "spf_records": $SPFRecords, + "dkim_config": $DKIMConfig, + "dkim_records": $DKIMRecords, + "dmarc_records": $DMARCRecords, + "transport_config": $TransportConfig, + "sharing_policy": $SharingPolicy, + "transport_rule": $TransportRules, + "conn_filter": $ConnectionFilter, + "org_config": $Config, + "exo_successful_commands": $SuccessfulCommands, + "exo_unsuccessful_commands": $UnSuccessfulCommands, +"@ + + # We need to remove the backslash characters from the + # json, otherwise rego gets mad. + $json = $json.replace("\`"", "'") + $json = $json.replace("\", "") + $json +} + +function Get-EXOTenantDetail { + <# + .Description + Gets the tenant details using the EXO PowerShell Module + .Functionality + Internal + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateSet("commercial", "gcc", "gcchigh", "dod", IgnoreCase = $false)] + [string] + $M365Environment + ) + try { + Import-Module ExchangeOnlineManagement + $OrgConfig = Get-OrganizationConfig -ErrorAction "Stop" + $DomainName = $OrgConfig.Name + $TenantId = "Error retrieving Tenant ID" + $Uri = "https://login.microsoftonline.com/$($DomainName)/.well-known/openid-configuration" + + if (($M365Environment -eq "gcchigh") -or ($M365Environment -eq "dod")) { + $TLD = ".us" + $Uri = "https://login.microsoftonline$($TLD)/$($DomainName)/.well-known/openid-configuration" + } + try { + $Content = (Invoke-WebRequest -Uri $Uri -ErrorAction "Stop").Content + $TenantId = (ConvertFrom-Json $Content).token_endpoint.Split("/")[3] + } + catch { + Write-Warning "Unable to retrieve EXO Tenant ID with URI. This may be caused by proxy error see 'Running the Script Behind Some Proxies' in the README for a solution. $($_)" + } + + $EXOTenantInfo = @{ + "DisplayName"= $OrgConfig.DisplayName; + "DomainName" = $DomainName; + "TenantId" = $TenantId; + "EXOAdditionalData" = $OrgConfig; + } + $EXOTenantInfo = ConvertTo-Json @($EXOTenantInfo) -Depth 4 + $EXOTenantInfo + } + catch { + Write-Warning "Error retrieving Tenant details using Get-EXOTenantDetail $($_)" + $EXOTenantInfo = @{ + "DisplayName" = "Error retrieving Display name"; + "DomainName" = "Error retrieving Domain name"; + "TenantId" = "Error retrieving Tenant ID"; + "EXOAdditionalData" = "Error retrieving additional data"; + } + $EXOTenantInfo = ConvertTo-Json @($EXOTenantInfo) -Depth 4 + $EXOTenantInfo + } +} +function Get-ScubaSpfRecords { + <# + .Description + Gets the SPF records for each domain in $Domains + .Functionality + Internal + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [System.Object[]] + $Domains + ) + + $SPFRecords = @() + + foreach ($d in $Domains) { try { $response = Resolve-DnsName $d.DomainName txt -ErrorAction Stop $rdata = @($response.Strings) } catch { - $rdata = "" + if ($_.FullyQualifiedErrorId -eq "DNS_ERROR_RCODE_NAME_ERROR,Microsoft.DnsClient.Commands.ResolveDnsName") { + # Error is expected, just means the SPF record does not exist, does not mean the command failed + $rdata = "" + } + else { + # Error is not expected, let the exception propagate + throw $_ + } } $DomainName = $d.DomainName @@ -46,13 +186,25 @@ function Export-EXOProvider { } } - $SPFRecords = ConvertTo-Json $SPFRecords + $SPFRecords +} +function Get-ScubaDkimRecords { <# - 2.3 DKIM + .Description + Gets the DKIM records for each domain in $Domains + .Functionality + Internal #> - $DKIMConfig = @(Get-DkimSigningConfig) + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [System.Object[]] + $Domains + ) + $DKIMRecords = @() + foreach ($d in $domains) { $DomainName = $d.DomainName $selectors = "selector1", "selector2" @@ -67,129 +219,89 @@ function Export-EXOProvider { break } catch { - continue + if ($_.FullyQualifiedErrorId -eq "DNS_ERROR_RCODE_NAME_ERROR,Microsoft.DnsClient.Commands.ResolveDnsName") { + # Error is expected, just means the DKIM record does not exist with this selector, + # we need to try again with a different one + continue + } + else { + # Error is not expected, let the exception propagate + throw $_ + } } } + $DKIMRecords += [PSCustomObject]@{ "domain" = $DomainName; "rdata" = "$rdata" } } - $DKIMRecords = ConvertTo-Json $DKIMRecords - $DKIMConfig = ConvertTo-Json $DKIMConfig + $DKIMRecords +} + +function Get-ScubaDmarcRecords { <# - 2.4 DMARC + .Description + Gets the DMARC records for each domain in $Domains + .Functionality + Internal #> + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [System.Object[]] + $Domains + ) + $DMARCRecords = @() foreach ($d in $domains) { try { + # First check to see if the record is available at the full domain level $DomainName = $d.DomainName $response = Resolve-DnsName "_dmarc.$DomainName" txt -ErrorAction Stop $rdata = $response.Strings } catch { - $Labels = $d.DomainName.Split(".") - try { - $Labels = $d.DomainName.Split(".") - $OrgDomain = $Labels[-2] + "." + $Labels[-1] - # Technically the logic above is incomplete. This will work when the tld is single - # label (e.g., com, org, gov). However, when the tld is two-labels (e.g., gov.uk), - # this will cause an error. Leaving cases like that as out-of-scope for now. - $response = Resolve-DnsName "_dmarc.$OrgDomain" txt -ErrorAction Stop - $rdata = $response.Strings + if ($_.FullyQualifiedErrorId -eq "DNS_ERROR_RCODE_NAME_ERROR,Microsoft.DnsClient.Commands.ResolveDnsName") { + # Error is expected, just means the domain does not exist, does not mean the command failed + # If the record is not available at the full domain level, we need to check at the + # organizational domain level + $Labels = $d.DomainName.Split(".") + try { + $Labels = $d.DomainName.Split(".") + $OrgDomain = $Labels[-2] + "." + $Labels[-1] + # Technically the logic above is incomplete. This will work when the tld is single + # label (e.g., com, org, gov). However, when the tld is two-labels (e.g., gov.uk), + # this will cause an error. Leaving cases like that as out-of-scope for now. + $response = Resolve-DnsName "_dmarc.$OrgDomain" txt -ErrorAction Stop + $rdata = $response.Strings } catch { - $rdata = "" + if ($_.FullyQualifiedErrorId -eq "DNS_ERROR_RCODE_NAME_ERROR,Microsoft.DnsClient.Commands.ResolveDnsName") { + # Error is expected, just means the dmarc record does not exist, does not mean + # the command failed + $rdata = "" + } + else { + # Error is not expected, let the exception propagate + throw $_ + } } } - - $DomainName = $d.DomainName - $DMARCRecords += [PSCustomObject]@{ - "domain" = $DomainName; - "rdata" = "$rdata" + else { + # Error is not expected, let the exception propagate + throw $_ } } - $DMARCRecords = ConvertTo-Json $DMARCRecords - <# - 2.5 - #> - - $TransportConfig = Get-TransportConfig - $TransportConfig.WhenChanged = $TransportConfig.WhenChanged.ToString() - $TransportConfig.WhenCreated = $TransportConfig.WhenCreated.ToString() - $TransportConfig.WhenChangedUTC = $TransportConfig.WhenChangedUTC.ToString() - $TransportConfig.WhenCreatedUTC = $TransportConfig.WhenCreatedUTC.ToString() - $TransportConfig = ConvertTo-Json $TransportConfig - - <# - 2.6 - #> - $SharingPolicy = Get-SharingPolicy - $SharingPolicy.WhenChanged = $SharingPolicy.WhenChanged.ToString() - $SharingPolicy.WhenCreated = $SharingPolicy.WhenCreated.ToString() - $SharingPolicy.WhenChangedUTC = $SharingPolicy.WhenChangedUTC.ToString() - $SharingPolicy.WhenCreatedUTC = $SharingPolicy.WhenCreatedUTC.ToString() - $SharingPolicy = ConvertTo-Json $SharingPolicy - - <# - 2.7 - #> - - $TransportRules = @(Get-TransportRule) - foreach ($Rule in $TransportRules) { - $Rule.WhenChanged = $Rule.WhenChanged.ToString() - } - $TransportRules = ConvertTo-Json $TransportRules - - <# - 2.12 - #> - - $ConnectionFilter = Get-HostedConnectionFilterPolicy | ConvertTo-Json - - <# - 2.13 - #> - $Config = Get-OrganizationConfig - $Config = $Config | ConvertTo-Json - - - <# - Save output - #> - $json = @" - "remote_domains": $RemoteDomains, - "spf_records": $SPFRecords, - "dkim_config": $DKIMConfig, - "dkim_records": $DKIMRecords, - "dmarc_records": $DMARCRecords, - "transport_config": $TransportConfig, - "sharing_policy": $SharingPolicy, - "transport_rule": $TransportRules, - "org_config": $Config, - "conn_filter": $ConnectionFilter, -"@ + $DomainName = $d.DomainName + $DMARCRecords += [PSCustomObject]@{ + "domain" = $DomainName; + "rdata" = "$rdata" + } + } - # We need to remove the backslash characters from the - # json, otherwise rego gets mad. - $json = $json.replace("\`"", "'") - $json = $json.replace("\", "") - $json -} - -function Get-EXOTenantDetail { - <# - .Description - Gets the tenant details using the EXO PowerShell Module - .Functionality - Internal - #> - Import-Module ExchangeOnlineManagement - $Config = Get-OrganizationConfig - $TenantInfo = @{"DisplayName"=$Config.Name;} - $TenantInfo = $TenantInfo | ConvertTo-Json -Depth 4 - $TenantInfo + $DMARCRecords } diff --git a/PowerShell/ScubaGear/Modules/Providers/ExportOneDriveProvider.psm1 b/PowerShell/ScubaGear/Modules/Providers/ExportOneDriveProvider.psm1 index eaca9cd74e..1bf25c837a 100644 --- a/PowerShell/ScubaGear/Modules/Providers/ExportOneDriveProvider.psm1 +++ b/PowerShell/ScubaGear/Modules/Providers/ExportOneDriveProvider.psm1 @@ -6,18 +6,39 @@ function Export-OneDriveProvider { .Functionality Internal #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateSet("commercial", "gcc", "gcchigh", "dod", IgnoreCase = $false)] + [string] + $M365Environment + ) + $HelperFolderPath = Join-Path -Path $PSScriptRoot -ChildPath "ProviderHelpers" + Import-Module (Join-Path -Path $HelperFolderPath -ChildPath "CommandTracker.psm1") + Import-Module (Join-Path -Path $HelperFolderPath -ChildPath "SPOSiteHelper.psm1") + $Tracker = Get-CommandTracker - $InitialDomain = (Get-MgOrganization).VerifiedDomains | Where-Object {$_.isInitial} + #Get InitialDomainPrefix + $InitialDomain = ($Tracker.TryCommand("Get-MgOrganization")).VerifiedDomains | Where-Object {$_.isInitial} $InitialDomainPrefix = $InitialDomain.Name.split(".")[0] - $SPOTenantInfo = Get-SPOTenant | ConvertTo-Json - $ExpectedResults = Get-SPOSite -Identity "https://$($InitialDomainPrefix).sharepoint.com/" | ConvertTo-Json - $TenantSyncInfo = Get-SPOTenantSyncClientRestriction | ConvertTo-Json + + #Get SPOSiteIdentity + $SPOSiteIdentity = Get-SPOSiteHelper -M365Environment $M365Environment -InitialDomainPrefix $InitialDomainPrefix + + $SPOTenantInfo = ConvertTo-Json @($Tracker.TryCommand("Get-SPOTenant")) + $ExpectedResults = ConvertTo-Json @($Tracker.TryCommand("Get-SPOSite", @{"Identity"="$($SPOSiteIdentity)"})) + $TenantSyncInfo = ConvertTo-Json @($Tracker.TryCommand("Get-SPOTenantSyncClientRestriction")) + + $SuccessfulCommands = ConvertTo-Json @($Tracker.GetSuccessfulCommands()) + $UnSuccessfulCommands = ConvertTo-Json @($Tracker.GetUnSuccessfulCommands()) # Note the spacing and the last comma in the json is important $json = @" "SPO_tenant_info": $SPOTenantInfo, "Expected_results": $ExpectedResults, "Tenant_sync_info": $TenantSyncInfo, + "OneDrive_successful_commands": $SuccessfulCommands, + "OneDrive_unsuccessful_commands": $UnSuccessfulCommands, "@ # We need to remove the backslash characters from the json, otherwise rego gets mad. diff --git a/PowerShell/ScubaGear/Modules/Providers/ExportPowerPlatformProvider.psm1 b/PowerShell/ScubaGear/Modules/Providers/ExportPowerPlatformProvider.psm1 index 426fe9f887..815d76cac0 100644 --- a/PowerShell/ScubaGear/Modules/Providers/ExportPowerPlatformProvider.psm1 +++ b/PowerShell/ScubaGear/Modules/Providers/ExportPowerPlatformProvider.psm1 @@ -8,7 +8,7 @@ function Export-PowerPlatformProvider { Internal #> - # Note importing the module might have to be done for every provider as + # Manually importing the module name here to bypass cmdlet name conflicts # There are conflicting PowerShell Cmdlet names in EXO and Power Platform Import-Module Microsoft.PowerApps.Administration.PowerShell -DisableNameChecking @@ -62,9 +62,51 @@ function Get-PowerPlatformTenantDetail { .Functionality Internal #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateSet("commercial", "gcc", "gcchigh", "dod", IgnoreCase = $false)] + [string] + $M365Environment + ) Import-Module Microsoft.PowerApps.Administration.PowerShell -DisableNameChecking - $TenantDetails = Get-TenantDetailsFromGraph - $TenantInfo = @{"DisplayName"=$TenantDetails.DisplayName;} - $TenantInfo = $TenantInfo | ConvertTo-Json -Depth 4 - $TenantInfo + + try { + $PowerTenantDetails = Get-TenantDetailsFromGraph -ErrorAction "Stop" + + $Domains = $PowerTenantDetails.Domains + $TenantDomain = "PowerPlatform: Domain Unretrievable" + $TLD = ".com" + if (($M365Environment -eq "gcchigh") -or ($M365Environment -eq "dod")) { + $TLD = ".us" + } + foreach ($Domain in $Domains) { + $Name = $Domain.Name + $IsInitial = $Domain.initial + $DomainChecker = $Name.EndsWith(".onmicrosoft$($TLD)") -and !$Name.EndsWith(".mail.onmicrosoft$($TLD)") -and $IsInitial + if ($DomainChecker){ + $TenantDomain = $Name + } + } + + $PowerTenantInfo = @{ + "DisplayName" = $PowerTenantDetails.DisplayName; + "DomainName" = $TenantDomain; + "TenantId" = $PowerTenantDetails.TenantId + "PowerPlatformAdditionalData" = $PowerTenantDetails; + } + $PowerTenantInfo = ConvertTo-Json @($PowerTenantInfo) -Depth 4 + $PowerTenantInfo + } + catch { + Write-Warning "Error retrieving Tenant details using Get-PowerPlatformTenantDetail $($_)" + $PowerTenantInfo = @{ + "DisplayName" = "Error retrieving Display name"; + "DomainName" = "Error retrieving Domain name"; + "TenantId" = "Error retrieving Tenant ID"; + "PowerPlatformAdditionalData" = "Error retrieving additional data"; + } + $PowerTenantInfo = ConvertTo-Json @($PowerTenantInfo) -Depth 4 + $PowerTenantInfo + } } diff --git a/PowerShell/ScubaGear/Modules/Providers/ExportSharePointProvider.psm1 b/PowerShell/ScubaGear/Modules/Providers/ExportSharePointProvider.psm1 index ef6c4cab03..9850ef95c4 100644 --- a/PowerShell/ScubaGear/Modules/Providers/ExportSharePointProvider.psm1 +++ b/PowerShell/ScubaGear/Modules/Providers/ExportSharePointProvider.psm1 @@ -6,17 +6,39 @@ function Export-SharePointProvider { .Functionality Internal #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateSet("commercial", "gcc", "gcchigh", "dod", IgnoreCase = $false)] + [string] + $M365Environment + ) + $HelperFolderPath = Join-Path -Path $PSScriptRoot -ChildPath "ProviderHelpers" + Import-Module (Join-Path -Path $HelperFolderPath -ChildPath "CommandTracker.psm1") + Import-Module (Join-Path -Path $HelperFolderPath -ChildPath "SPOSiteHelper.psm1") + $Tracker = Get-CommandTracker - $InitialDomain = (Get-MgOrganization).VerifiedDomains | Where-Object {$_.isInitial} + #Get InitialDomainPrefix + $InitialDomain = ($Tracker.TryCommand("Get-MgOrganization")).VerifiedDomains | Where-Object {$_.isInitial} $InitialDomainPrefix = $InitialDomain.Name.split(".")[0] - $SPOTenant = Get-SPOTenant | ConvertTo-Json - $SPOSite = Get-SPOSite -Identity "https://$($InitialDomainPrefix).sharepoint.com/" -detailed | Select-Object -Property * | ConvertTo-Json + + #Get SPOSiteIdentity + $SPOSiteIdentity = Get-SPOSiteHelper -M365Environment $M365Environment -InitialDomainPrefix $InitialDomainPrefix + + $SPOTenant = ConvertTo-Json @($Tracker.TryCommand("Get-SPOTenant")) + $SPOSite = ConvertTo-Json @($Tracker.TryCommand("Get-SPOSite", @{"Identity"="$($SPOSiteIdentity)"; "Detailed"=$true}) | Select-Object -Property *) + + $SuccessfulCommands = ConvertTo-Json @($Tracker.GetSuccessfulCommands()) + $UnSuccessfulCommands = ConvertTo-Json @($Tracker.GetUnSuccessfulCommands()) # Note the spacing and the last comma in the json is important $json = @" "SPO_tenant": $SPOTenant, "SPO_site": $SPOSite, + "SharePoint_successful_commands": $SuccessfulCommands, + "SharePoint_unsuccessful_commands": $UnSuccessfulCommands, "@ + # We need to remove the backslash characters from the json, otherwise rego gets mad. $json = $json.replace("\`"", "'") $json = $json.replace("\", "") diff --git a/PowerShell/ScubaGear/Modules/Providers/ExportTeamsProvider.psm1 b/PowerShell/ScubaGear/Modules/Providers/ExportTeamsProvider.psm1 index 044bbcd03a..663fffc577 100644 --- a/PowerShell/ScubaGear/Modules/Providers/ExportTeamsProvider.psm1 +++ b/PowerShell/ScubaGear/Modules/Providers/ExportTeamsProvider.psm1 @@ -8,12 +8,19 @@ function Export-TeamsProvider { #> [CmdletBinding()] - $TenantInfo = ConvertTo-Json @(Get-CsTenant) - $MeetingPolicies = ConvertTo-Json @(Get-CsTeamsMeetingPolicy) - $FedConfig = ConvertTo-Json @(Get-CsTenantFederationConfiguration) - $ClientConfig = ConvertTo-Json @(Get-CsTeamsClientConfiguration) - $AppPolicies = ConvertTo-Json @(Get-CsTeamsAppPermissionPolicy) - $BroadcastPolicies = ConvertTo-Json @(Get-CsTeamsMeetingBroadcastPolicy) + $HelperFolderPath = Join-Path -Path $PSScriptRoot -ChildPath "ProviderHelpers" + Import-Module (Join-Path -Path $HelperFolderPath -ChildPath "CommandTracker.psm1") + $Tracker = Get-CommandTracker + + $TenantInfo = ConvertTo-Json @($Tracker.TryCommand("Get-CsTenant")) + $MeetingPolicies = ConvertTo-Json @($Tracker.TryCommand("Get-CsTeamsMeetingPolicy")) + $FedConfig = ConvertTo-Json @($Tracker.TryCommand("Get-CsTenantFederationConfiguration")) + $ClientConfig = ConvertTo-Json @($Tracker.TryCommand("Get-CsTeamsClientConfiguration")) + $AppPolicies = ConvertTo-Json @($Tracker.TryCommand("Get-CsTeamsAppPermissionPolicy")) + $BroadcastPolicies = ConvertTo-Json @($Tracker.TryCommand("Get-CsTeamsMeetingBroadcastPolicy")) + + $TeamsSuccessfulCommands = ConvertTo-Json @($Tracker.GetSuccessfulCommands()) + $TeamsUnSuccessfulCommands = ConvertTo-Json @($Tracker.GetUnSuccessfulCommands()) # Note the spacing and the last comma in the json is important $json = @" @@ -23,6 +30,8 @@ function Export-TeamsProvider { "client_configuration": $ClientConfig, "app_policies": $AppPolicies, "broadcast_policies": $BroadcastPolicies, + "teams_successful_commands": $TeamsSuccessfulCommands, + "teams_unsuccessful_commands": $TeamsUnSuccessfulCommands, "@ # We need to remove the backslash characters from the @@ -39,14 +48,51 @@ function Get-TeamsTenantDetail { .Functionality Internal #> - $TenantInfo = Get-CsTenant - + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateSet("commercial", "gcc", "gcchigh", "dod", IgnoreCase = $false)] + [string] + $M365Environment + ) # Need to explicitly clear or convert these values to strings, otherwise # these fields contain values Rego can't parse. - $TenantInfo.AssignedPlan = @() - $TenantInfo.LastSyncTimeStamp = $TenantInfo.LastSyncTimeStamp.ToString() - $TenantInfo.WhenChanged = $TenantInfo.WhenChanged.ToString() - $TenantInfo.WhenCreated = $TenantInfo.WhenCreated.ToString() - $TenantInfo = ConvertTo-Json @($TenantInfo) -Depth 4 - $TenantInfo + try { + $TenantInfo = Get-CsTenant -ErrorAction "Stop" + + $VerifiedDomains = $TenantInfo.VerifiedDomains + $TenantDomain = "Teams: Domain Unretrievable" + $TLD = ".com" + if (($M365Environment -eq "gcchigh") -or ($M365Environment -eq "dod")) { + $TLD = ".us" + } + foreach ($Domain in $VerifiedDomains.GetEnumerator()) { + $Name = $Domain.Name + $Status = $Domain.Status + $DomainChecker = $Name.EndsWith(".onmicrosoft$($TLD)") -and !$Name.EndsWith(".mail.onmicrosoft$($TLD)") -and $Status -eq "Enabled" + if ($DomainChecker) { + $TenantDomain = $Name + } + } + + $TeamsTenantInfo = @{ + "DisplayName" = $TenantInfo.DisplayName; + "DomainName" = $TenantDomain; + "TenantId" = $TenantInfo.TenantId; + "TeamsAdditionalData" = $TenantInfo; + } + $TeamsTenantInfo = ConvertTo-Json @($TeamsTenantInfo) -Depth 4 + $TeamsTenantInfo + } + catch { + Write-Warning "Error retrieving Tenant details using Get-TeamsTenantDetail $($_)" + $TeamsTenantInfo = @{ + "DisplayName" = "Error retrieving Display name"; + "DomainName" = "Error retrieving Domain name"; + "TenantId" = "Error retrieving Tenant ID"; + "TeamsAdditionalData" = "Error retrieving additional data"; + } + $TeamsTenantInfo = ConvertTo-Json @($TeamsTenantInfo) -Depth 4 + $TeamsTenantInfo + } } diff --git a/PowerShell/ScubaGear/Modules/Providers/ProviderHelpers/CommandTracker.psm1 b/PowerShell/ScubaGear/Modules/Providers/ProviderHelpers/CommandTracker.psm1 new file mode 100644 index 0000000000..ce50990684 --- /dev/null +++ b/PowerShell/ScubaGear/Modules/Providers/ProviderHelpers/CommandTracker.psm1 @@ -0,0 +1,65 @@ +Import-Module -Name $PSScriptRoot/../ExportEXOProvider.psm1 -Function Get-ScubaSpfRecords, Get-ScubaDkimRecords, Get-ScubaDmarcRecords + +class CommandTracker { + [string[]]$SuccessfulCommands = @() + [string[]]$UnSuccessfulCommands = @() + + [System.Object[]] TryCommand([string]$Command, [hashtable]$CommandArgs) { + <# + .Description + Wraps the given Command inside a try/catch, run with the provided + arguments, and tracks successes/failures. Unless otherwise specified, + ErrorAction defaults to "Stop" + .Functionality + Internal + #> + if (-Not $CommandArgs.ContainsKey("ErrorAction")) { + $CommandArgs.ErrorAction = "Stop" + } + + try { + $Result = & $Command @CommandArgs + $this.SuccessfulCommands += $Command + return $Result + } + catch { + Write-Warning "Error running $($Command). $($_)" + $this.UnSuccessfulCommands += $Command + $Result = @() + return $Result + } + } + + [System.Object[]] TryCommand([string]$Command) { + <# + .Description + Wraps the given Command inside a try/catch and tracks successes/ + failures. No command arguments are specified beyond ErrorAction=Stop + .Functionality + Internal + #> + + return $this.TryCommand($Command, @{}) + } + + [void] AddSuccessfulCommand([string]$Command) { + $this.SuccessfulCommands += $Command + } + + [void] AddUnSuccessfulCommand([string]$Command) { + $this.UnSuccessfulCommands += $Command + } + + [string[]] GetUnSuccessfulCommands() { + return $this.UnSuccessfulCommands + } + + [string[]] GetSuccessfulCommands() { + return $this.SuccessfulCommands + } + +} + +function Get-CommandTracker { + [CommandTracker]::New() +} \ No newline at end of file diff --git a/PowerShell/ScubaGear/Modules/Providers/ProviderHelpers/SPOSiteHelper.psm1 b/PowerShell/ScubaGear/Modules/Providers/ProviderHelpers/SPOSiteHelper.psm1 new file mode 100644 index 0000000000..066d667c09 --- /dev/null +++ b/PowerShell/ScubaGear/Modules/Providers/ProviderHelpers/SPOSiteHelper.psm1 @@ -0,0 +1,39 @@ +function Get-SPOSiteHelper { + <# + .Description + This function is used for assisting in connecting to different M365 Environments for EXO. + .Functionality + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, ParameterSetName = 'Report')] + [ValidateSet("commercial", "gcc", "gcchigh", "dod")] + [string] + $M365Environment, + + [Parameter(Mandatory = $true, ParameterSetName = 'Report')] + [string] + $InitialDomainPrefix + ) + $SPOSiteIdentity = "" + switch ($M365Environment) { + {"commercial" -or "gcc"} { + $SPOSiteIdentity = "https://$($InitialDomainPrefix).sharepoint.com/" + } + "gcchigh" { + $SPOSiteIdentity = "https://$($InitialDomainPrefix).sharepoint.us/" + } + "dod" { + $SPOSiteIdentity = "https://$($InitialDomainPrefix).sharepoint-mil.us/" + } + default { + Write-Error -Message "Unsupported or invalid M365Environment argument" + } + } + $SPOSiteIdentity +} + +Export-ModuleMember -Function @( + 'Get-SPOSiteHelper' +) \ No newline at end of file diff --git a/PowerShell/ScubaGear/RequiredVersions.ps1 b/PowerShell/ScubaGear/RequiredVersions.ps1 new file mode 100644 index 0000000000..946547c84d --- /dev/null +++ b/PowerShell/ScubaGear/RequiredVersions.ps1 @@ -0,0 +1,93 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'ModuleList')] +$ModuleList = @( + @{ + ModuleName = 'MicrosoftTeams' + ModuleVersion = [version] '4.8.0' + MaximumVersion = [version] '4.99.99999' + }, + @{ + ModuleName = 'ExchangeOnlineManagement' # includes Defender + ModuleVersion = [version] '3.0.0' + MaximumVersion = [version] '3.99.99999' + }, + @{ + ModuleName = 'Microsoft.Online.SharePoint.PowerShell' # includes OneDrive + ModuleVersion = [version] '16.0.0' + MaximumVersion = [version] '16.99.99999' + }, + @{ + ModuleName = 'Microsoft.PowerApps.Administration.PowerShell' + ModuleVersion = [version] '2.0.0' + MaximumVersion = [version] '2.99.99999' + }, + @{ + ModuleName = 'Microsoft.PowerApps.PowerShell' + ModuleVersion = [version] '1.0.0' + MaximumVersion = [version] '1.99.99999' + }, + @{ + ModuleName = 'Microsoft.Graph.Applications' #TODO: Verify is needed + ModuleVersion = [version] '1.14.0' + MaximumVersion = [version] '1.99.99999' + }, + @{ + ModuleName = 'Microsoft.Graph.Authentication' + ModuleVersion = [version] '1.14.0' + MaximumVersion = [version] '1.99.99999' + }, + @{ + ModuleName = 'Microsoft.Graph.DeviceManagement' #TODO: Verify is needed + ModuleVersion = [version] '1.14.0' + MaximumVersion = [version] '1.99.99999' + }, + @{ + ModuleName = 'Microsoft.Graph.DeviceManagement.Administration' #TODO: Verify is needed + ModuleVersion = [version] '1.14.0' + MaximumVersion = [version] '1.99.99999' + }, + @{ + ModuleName = 'Microsoft.Graph.DeviceManagement.Enrolment' #TODO: Verify is needed + ModuleVersion = [version] '1.14.0' + MaximumVersion = [version] '1.99.99999' + }, + @{ + ModuleName = 'Microsoft.Graph.Devices.CorporateManagement' #TODO: Verify is needed + ModuleVersion = [version] '1.14.0' + MaximumVersion = [version] '1.99.99999' + }, + @{ + ModuleName = 'Microsoft.Graph.Groups' + ModuleVersion = [version] '1.14.0' + MaximumVersion = [version] '1.99.99999' + }, + @{ + ModuleName = 'Microsoft.Graph.Identity.DirectoryManagement' + ModuleVersion = [version] '1.14.0' + MaximumVersion = [version] '1.99.99999' + }, + @{ + ModuleName = 'Microsoft.Graph.Identity.Governance' #TODO: Verify is needed + ModuleVersion = [version] '1.14.0' + MaximumVersion = [version] '1.99.99999' + }, + @{ + ModuleName = 'Microsoft.Graph.Identity.SignIns' + ModuleVersion = [version] '1.14.0' + MaximumVersion = [version] '1.99.99999' + }, + @{ + ModuleName = 'Microsoft.Graph.Planner' #TODO: Verify is needed + ModuleVersion = [version] '1.14.0' + MaximumVersion = [version] '1.99.99999' + }, + @{ + ModuleName = 'Microsoft.Graph.Teams' #TODO: Verify is needed + ModuleVersion = [version] '1.14.0' + MaximumVersion = [version] '1.99.99999' + }, + @{ + ModuleName = 'Microsoft.Graph.Users' + ModuleVersion = [version] '1.14.0' + MaximumVersion = [version] '1.99.99999' + } +) \ No newline at end of file diff --git a/PowerShell/ScubaGear/ScubaGear.psd1 b/PowerShell/ScubaGear/ScubaGear.psd1 index 4738ea2dbcca0465317058f33006061367d63863..67a219ede486b56d2d8c90d331680f27e0ee4bd5 100644 GIT binary patch literal 4120 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 **Warning** > This tool is in an alpha state and in active development. At this time, outputs could be incorrect and should be reviewed carefully. ## M365 Product License Assumptions -This tool was tested against tenants that have an E3 or G3 and E5 or G5 M365 license bundle. It may still function for tenants that do not have one of these bundles but it was not specifically tested that way. Also, some of the specific policy checks in the baseline rely on the following specific security licenses which are included by default in E5 and G5. If your tenant does not have the security licenses listed below, the report will display a non-compliant output for those respective policy checks. +This tool was tested against tenants that have an M365 E3 or G3 and E5 or G5 license bundle. It may still function for tenants that do not have one of these bundles. + +Some of the policy checks in the baseline rely on the following licenses which are included by default in M365 E5 and G5. - Azure AD Premium Plan 2 - Microsoft Defender for Office 365 Plan 1 -## Project License +If a tenant does not have the licenses listed above, the report will display a non-compliant output for those policies. -Unless otherwise noted, this project is distributed under the Creative Commons Zero license. With developer approval, contributions may be submitted with an alternate compatible license. If accepted, those contributions will be listed herein with the appropriate license. +> **Note**: GCC-High/DOD endpoints are included, but have not been tested. Please open an issue if you encounter bugs. GCC-High testing in progress. ## Installation +### Downloading Repository +To download ScubaGear: + +1. Click [here](https://github.com/cisagov/ScubaGear/releases/latest) to see the latest release. +2. Click `ScubaGear-v0-2-0.zip` (or latest version) to download the release. +3. Extract the folder in the zip file. + ### Installing the required PowerShell Modules -To install the required modules, execute the Powershell script `.\SetUp.ps1` (in the project's root folder) as in the example below. -**Warning**: Note that -AllowClobber -Force flags are currently included in that script. These flags will install the latest available version of all the required Powershell modules. If you need to keep earlier versions of the modules for other software that you use, modify the script file SetUp.ps1 and remove those two flags. +> **Note**: Only PowerShell 5.1 is currently supported. PowerShell 7 may work, but has not been tested. PowerShell 7 will be added in a future release. -``` -.\Setup.ps1 +To import the module, open a new PowerShell 5.1 terminal and navigate to the repository folder. + +Depending on the [PowerShell execution policy](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-5.1), running `Unblock-File` on the ScubaGear folder may be required. See [here](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/unblock-file?view=powershell-5.1) for more information. + +Then run: + +```powershell +.\Setup.ps1 #Installs the required modules +Import-Module -Name .\PowerShell\ScubaGear #Imports the tool into your session ``` ## Usage -To run the assessment tool, customize and then execute the Powershell script `.\RunSCuBA.ps1` (in the project's root folder). +### Example 1: Run an assessment against all products (except PowerPlatform) -In `RunSCuBA.ps1` there are execution variables that you must customize by entering values that match your specific M365 tenant. The variables are described below, along with execution examples. +```powershell +Invoke-SCuBA +``` +### Example 2: Run an assessment against Azure Active Directory with custom report output location +```powershell +Invoke-SCuBA -ProductNames aad -OutPath C:\Users\johndoe\reports +``` +### Example 3: Run assessments against multiple products +```powershell +Invoke-SCuBA -ProductNames aad, spo, teams +``` + +To view more examples and see detailed help run: +```powershell +Get-Help -Name Invoke-SCuBA -Full +``` -### Variable Definitions +### Parameter Definitions -- **$LogIn \-** is a `$true` or `$false` variable that if set to `$true` will prompt you to provide credentials if you want to establish a connection to the specified M365 products in the **$ProductNames** variable. For most use cases, leave this variable to be `$true`. A connection is established in the current PowerShell terminal session with the first authentication. If you want to run another verification in the same PowerShell session simply set this variable to be `$false` to bypass the reauthenticating in the same session. +- **$LogIn** is a `$true` or `$false` variable that if set to `$true` will prompt the user to provide credentials to establish a connection to the specified M365 products in the **$ProductNames** variable. For most use cases, leave this variable to be `$true`. A connection is established in the current PowerShell terminal session with the first authentication. To run another verification in the same PowerShell session, set this variable to be `$false` to bypass the need to authenticate again in the same session. Note: defender will ask for authentication even if this variable is set to `$false` - **$ProductNames** is a list of one ore more M365 shortened product names that the tool will assess when it is executed. Acceptable product name values are listed below. To assess Azure Active Directory you would enter the value **aad**. To assess Exchange Online you would enter **exo** and so forth. - Azure Active Directory: **aad** - Defender for Office 365: **defender** - Exchange Online: **exo** - OneDrive: **onedrive** - - MS Power Platform: **powerplatform** + - Power Platform: **powerplatform** - SharePoint Online: **sharepoint** - - MS Teams: **teams** + - Teams: **teams** -- **$Endpoint** is a variable used to authenticate to Power Platform. This variable is only mandatory if **powerplatform** is included in **$ProductNames**. Valid values include "dod", "prod","preview","tip1", "tip2", "usgov", or "usgovhigh". For M365 tenants with E3/E5 licenses enter the value **"prod"**. For M365 tenants with G3/G5 licenses enter the value **"usgov"**. +- **$M365Environment** parameter is used to authenticate to the various M365 commercial/ government environments. Valid values include `commercial`, `gcc`, `gcchigh`, or `dod`. Default value is `commercial`. + - For M365 tenants that are non-government environments enter the value `commercial`. + - For M365 Government Commercial Cloud tenants with G3/G5 licenses enter the value `gcc`. + - For M365 Government Commercial Cloud High tenants enter the value `gcchigh`. + - For M365 Department of Defense tenants enter the value `dod`. -- **$OPAPath** is a variable that refers to the folder location of the OPA Rego executable file. By default the OPA Rego executable embedded with this project is located in the project's root folder `"./"` and for most cases you won't need to modify this variable value. If you want to execute the tool using a version of OPA Rego located in another folder, then customize the variable value with the full path to the alternative OPA Rego exe file. -- **$OutPath** is a variable that refers to the folder path where both the output JSON and the HTML report will be created. By default the Reports folder is created in the same directory where the script is executed so you only need to modify this if you want the reports to be placed in an alternative location. The folder will be created if it does not exist. +- **$OPAPath** refers to the folder location of the Open Policy Agent (OPA) policy engine executable file. By default the OPA policy engine executable embedded with this project is located in the project's root folder `"./"` and for most cases this value will not need to be modified. To execute the tool using a version of the OPA policy engine located in another folder, customize the variable value with the full path to the folder containing the OPA policy engine executable file. -### Example Runs +- **$OutPath** refers to the folder path where the output JSON and the HTML report will be created. Defaults to the same directory where the script is executed. This parameter is only necessary if an alternate report folder path is desired. The folder will be created if it does not exist. -The example edited variable $ProductNames below in `RunSCuBA.ps1` will run the tool against the MS Teams security baseline -``` -$LogIn = $true -$ProductNames = @("teams") -$Endpoint = "" -$OPAPath = "./" -$OutPath = "./Reports" -``` +### Viewing the Report +The HTML report should open in your browser once the script completes. If it does not, navigate to the output folder and open the BaselineReports.html file using your browser. The result files generated from the tool are also saved to the output folder. -After these values are in place and `RunSCuBA.ps1` is saved. In a PowerShell terminal in the same directory as `RunSCuBA.ps1` enter -``` -.\RunSCuBA.ps1 -``` +## Required Permissions +The tool has two types of permissions that are required: +- User Permissions (which are associated with Azure AD roles assigned to a user) +- Application Permissions (which are assigned to the MS Graph PowerShell application in Azure AD). -The example edited variables $ProductNames and $OutPath below in `RunSCuBA.ps1` will run the tool against the Azure Active Directory and SharePoint Online baselines in an M365 tenant with the primary domain contoso.onmicrosoft.com. Notice that the output path for the reports has been customized. -``` -$LogIn = $true -$ProductNames = @("aad", "sharepoint") -$Endpoint = "" -$OPAPath = "./" -$OutPath = "C:\Users\mgibson\documents\Reports" -``` +### User Permissions +The minimum user roles needed for each product are described in the table below. -The example edited variables $ProductNames and $Endpoint below in `RunSCuBA.ps1` will run the tool against all possible product security baselines in an M365 G5 tenant with the primary domain example.onmicrosoft.com. +[This article](https://learn.microsoft.com/en-us/microsoft-365/admin/add-users/assign-admin-roles?view=o365-worldwide) also explains how to assign admin roles in M365. + +| Product | Role | +|-------------------------|:-----------------------------------------------------------------------------------:| +| Azure Active Directory | Global Reader | +| Teams | Global Reader (or Teams Administrator) | +| Exchange Online | Global Reader (or Exchange Administrator) | +| Defender for Office 365 | Global Reader (or Exchange Administrator) | +| Power Platform | Power Platform Administrator and a "Power Apps for Office 365" license | +| Sharepoint Online | SharePoint Administrator | +| OneDrive | SharePoint Administrator | + +**Note**: Users with the Global Administrator role always have the necessary user permissions to run the tool. -``` -$LogIn = $true -$ProductNames = @("aad", "defender", "exo", "onedrive", "powerplatform", "sharepoint", "teams") -$Endpoint = "usgov" -$OPAPath = "./" -$OutPath = "./Reports" -``` -### Viewing the Report -The html report should open automatically in your browser once the script completes. If it does not, navigate to the output folder and open the BaselineReports.html file using your browser. The result files generated from the tool are also saved to the output folder. -## Required User Permissions to Execute the tool -The tool has two types of permissions that are required to execute: User Permissions (which are associated with Azure AD roles assigned to a user) and Application Permissions (which are assigned to the MS Graph Powershell application in Azure AD). The minimum user roles needed to execute the tool against each of the products in M365 are described in the bullets below. The application permissions needed to execute against the Azure AD baseline and the process to setup those permissions are described in the section named "MS Graph Powershell permissions". If you run against the Power Platform product you will also need to have a "Power Apps for Office 365" license assigned to you. Before running the tool, check with your M365 administrator to ensure you have the required Azure AD roles. +### Microsoft Graph Powershell SDK permissions +The Azure AD baseline requires the use of Microsoft Graph. The script will attempt to configure the required API permissions needed by the Microsoft Graph PowerShell module, if they have not already been configured in the target tenant. -Note: If you are running the tool as a user with the Global Administrator role, you have the necessary user permissions and no additional roles should be necessary. No license assignments are needed either. The role requirements listed below are only applicable when running the tool with a user that is NOT Global Administrator. +The process to configure the application permissions is sometimes referred to as the "application consent process" because an Administrator must "consent" for the Microsoft Graph PowerShell application to access the tenant and the necessary Graph APIs to extract the configuration data. Depending on the Azure AD roles assigned to the user running the tool and how the application consent settings are configured in the target tenant, the process may vary slightly. To understand the application consent process, read [this article](https://learn.microsoft.com/en-us/azure/active-directory/develop/application-consent-experience) from Microsoft. -- Azure Active Directory: User must have the Global Reader role. -- MS Teams: User must have the Teams Administrator role. -- Exchange Online: User must have the Exchange Administrator role. -- Defender for Office 365: User must have the Exchange Administrator role. -- MS Power Platform: User must have the Power Platform Administrator role. User must also have the "Power Apps for Office 365" license. -- Sharepoint Online: User must have the SharePoint Administrator role. -- OneDrive: User must have the SharePoint Administrator role. +Microsoft Graph is used, because Azure AD PowerShell is being deprecated. -## MS Graph Powershell permissions to run Azure AD Baseline -When executing the tool against the Azure AD baseline for the first time, the script will attempt to configure the required API permissions needed by the MS Graph Powershell module if they have not already been configured in your tenant. The process to configure the API permissions is sometimes referred to as the "application consent process" because an administrator must "consent" for the MS Graph Powershell application to access the tenant and the necessary Graph APIs to extract the configuration data. Depending on the Azure AD roles assigned to you and how the application consent settings are configured in your tenant, the process may vary slightly. This section describes various first time usage scenarios that may occur depending on the roles assigned to you and how to respond for each scenario. +> **Note** +> Microsoft Graph PowerShell SDK appears as "unverified" on the AAD application consent screen. This is a [known issue](https://github.com/microsoftgraph/msgraph-sdk-powershell/issues/482). -For reference, the following API permissions are used by the tool via the MS Graph Powershell application: +The following API permissions are required for Microsoft Graph Powershell: - Directory.Read.All - GroupMember.Read.All - Organization.Read.All - Policy.Read.All -- Policy.ReadWrite.AuthenticationMethod - RoleManagement.Read.Directory - User.Read.All - UserAuthenticationMethod.Read.All -### Scenario 1 - You need to request approval via a workflow process -When executing for the first time, if you see a screen named Approval Required that has a Request Approval button on it, enter a justification and click Request Approval. -![Request approval screen 1](images/regularuserrequestapproval.PNG) -![Request approval screen 2](images/regularuserrequestapproval2.PNG) - -The script will abort and you will see a permissions error. Next notify your administrator that your request was submitted. The administrator should go to the Azure AD > Enterprise Applications > Activity > Admin Consent Requests page. On that page the administrator should see the request for Microsoft Graph Powershell, click on it to open up the Details page, then click Review Permissions and Consent. The administrator should then review the list of permissions requested and click the Accept button if the permissions are acceptable per the organization's security policies. - -![Admin application consent screen 1](images/adminconsentapproval1.PNG) -![Admin application consent screen 2](images/adminconsentapproval2.PNG) -![Admin application consent screen 3](images/adminconsentapproval3.PNG) +## Architecture +![SCuBA Architecture diagram](/images/scuba-architecture.png) +The tool employs a three-step process: +1. **Extract & Export**. In this step, we utilize the various PowerShell modules authored by Microsoft to export and serialize all the relevant settings into JSON. +2. **Test & Record**. Compare the exported settings from the previous step with the configuration prescribed in the baselines. This is done using [OPA Rego](https://www.openpolicyagent.org/docs/latest/policy-language/#what-is-rego), a declarative query language for defining policy. OPA provides a ready-to-use policy engine executable and version v0.41.0 is already included in this repository. The code for the ScubaGear tool was tested against the included version of OPA. To use a later version of the OPA policy engine, follow the instructions listed [here](https://www.openpolicyagent.org/docs/latest/#running-opa) and customize the `$OPAPath` variable described in the Usage section above. +3. **Format & Report**. Package the data output by the OPA policy engine into a human-friendly HTML report. -To verify that Microsoft Graph Powershell was properly configured, the administrator can navigate to the Azure AD > Enterprise Applications > Manage > All Applications > Microsoft Graph Powershell > Security > Permissions page and it should look similar to the screenshot below. +## Repository Organization +- `PowerShell` contains the code used to export the configuration settings from the M365 tenant and orchestrate the entire process from export through evaluation to report. The main PowerShell module manifest `SCuBA.psd1` is located in the PowerShell folder. +- `Rego` holds the `.rego` files. Each Rego file audits against the desired state for each product, per the SCuBA M365 secure configuration baseline documents. +- `Testing` contains code that is used during the development process to unit test Rego policies. -![Admin application consent screen 4](images/adminconsentapproval4.PNG) +## Project License -Once the administrator has completed the consent, you can re-execute the tool and it should successfully complete without any permissions errors. +Unless otherwise noted, this project is distributed under the Creative Commons Zero license. With developer approval, contributions may be submitted with an alternate compatible license. If accepted, those contributions will be listed herein with the appropriate license. -### Scenario 2 - You need Admin approval but the tenant does not have a workflow process configured -When executing for the first time, you may see a screen named Need Admin Approval. -![Admin approval but no workflow screen 1](images/regularuserneedadminapproval.PNG) +## Troubleshooting -If that screen appears, perform the following steps. -Ask your administrator to download the tool and then execute it against the Azure AD baseline. +### Executing against multiple tenants +ScubaGear creates connections to several M365 services. If running against multiple tenants, it is necessary to disconnect those sessions. -``` -$LogIn = $true -$ProductNames = @("aad") -$Endpoint = "" -$OPAPath = "./" -$OutPath = "./Reports" +`Invoke-SCuBA` includes the `-DisconnectOnExit` parameter to disconnect each of connection upon exit. To disconnect sessions after a run, use `Disconnect-SCuBATenant`. The cmdlet disconnects from Azure Active Directory (via MS Graph API), Defender, Exchange Online, OneDrive, Power Platform, SharePoint Online, and Microsoft Teams. ---- -.\RunSCuBA.ps1 +```PowerShell +Disconnect-SCuBATenant ``` -When the administrator runs the tool, they will be prompted to perform the consent process. The administrator should see a screen named Permissions Requested. The administrator should select "Consent on behalf of your organization" and click the Accept button. - -![Admin approve permissions for user screen 1](images/adminconsentworkflownotconfigured.PNG) - -Once the administrator has completed these steps, you can re-execute the tool and it should successfully complete without any permissions errors. - -### Scenario 3 - A user with Global Administrator privileges runs the automation tool -When executing for the first time, if your user account has the Global Administrator role, the consent process is abbreviated compared to scenarios 1 and 2. You should see a screen named Permissions Requested similar to the one below. - -![Global admin runs tool screen 1](images/globaladminuserrunningscript.PNG) - -On the Permissions Requested screen you have two options: -- Option 1: Configure the permissions for your user account only - if you want to do this, do NOT click the "Consent on behalf of your organization" checkbox. Simply click the Accept button. -- Option 2: Configure the permissions so that other users in the tenant can also execute the tool - if you want to do this, click the "Consent on behalf of your organization" checkbox and then the Accept button. - -Then the script should execute without any permissions errors. - -### Scenario 4 - A user with Cloud Application Administrator privileges runs the automation tool -When executing for the first time, if your user account has the Cloud Application Administrator role, the consent process is abbreviated compared to scenarios 1 and 2. You can only configure the tool to run with your user account. You should see a screen named Permissions Requested similar to the one below. - -![Cloud App admin runs tool screen 1](images/cloudadminuserrunningscript.PNG) - -On the Permissions Requested screen click the Accept button. Then the script should execute without any permissions errors. - -## Design -The tool employs a three-step process: -1. **Export**. In this step, we utilize the various PowerShell modules authored by Microsoft to export and serialize all the relevant settings into json. -2. **Parse**. Compare the exported settings from the previous step with the configuration prescribed in the baselines. We do this using [OPA Rego](https://www.openpolicyagent.org/docs/latest/policy-language/#what-is-rego), a declarative query language for defining policy. OPA provides a ready-to-use executable and version v0.41.0 is already included in this repo. The code for our tool was tested against the included version of OPA. If you desire to use a later version of Rego, follow the instructions listed [here](https://www.openpolicyagent.org/docs/latest/#running-opa) and customize the $OPAPath variable described in the Usage section above. -3. **Report**. Package the data output by Rego into a human-friendly html report. - -## File Organization -- The PowerShell folder contains the code used to export the configuration settings from the M365 tenant and orchestrate the entire process from export to parse to report. The main PowerShell module manifest `SCuBA.psd1` is located in the PowerShell folder. The RunSCuBA.ps1 script located in the root folder uses that module to execute the tool. -- The Rego folder holds the .rego files. Each rego file essentially audits against the "desired state" for each product, per the SCuBA M365 secure configuration baseline documents. -- The Reporter folder contains code and supporting files used to create and format the output report. -- The Testing folder contains code that is used during the development flow to unit test the Rego policies. - -## Troubleshooting +> The cmdlet will attempt to disconnect from all services regardless of current session state. Only connections established within the current PowerShell session will be disconnected and removed. Services that are already disconnected will not generate an error. ### Errors connecting to Defender If when running the tool against Defender (via ExchangeOnlineManagement PowerShell Module), you may see the connection error "Create Powershell Session is failed using OAuth" in the Powershell window, follow the instructions in this section. An example of the full error message is provided below. @@ -204,7 +177,7 @@ We provide a convenience script named `.\AllowBasicAuthentication.ps1`, in the r ### Exchange Online maximum connections error If when running the tool against Exchange Online, you see the error below in the Powershell window, follow the instructions in this section. -``` +```PowerShell New-ExoPSSession : Processing data from remote server outlook.office365.com failed with the following error message: [AuthZRequestId=8feccdea-493c-4c12-85dd-d185232cc0be][FailureCategory=A uthZ-AuthorizationException] Fail to create a runspace because you have exceeded the maximum @@ -212,10 +185,15 @@ number of connections allowed : 3 ``` If you see the error above run the command below in Powershell: -``` +```PowerShell Disconnect-ExchangeOnline ``` +or alternatively run `Disconnect-SCuBATenant` exported by the ScubaGear module. +```PowerShell +Disconnect-SCuBATenant +``` + ### Power Platform empty policy in report In order for the tool to properly assess the Power Platform product, one of the following conditions must be met: * The tenant includes the `Power Apps for Office 365` license AND the user running the tool has the `Power Platform Administrator` role assigned @@ -225,12 +203,82 @@ If these conditions are not met, the tool will generate an incorrect report outp ![Power Platform missing license](images/pplatformmissinglicense.PNG) -### Connect-MgGraph : Key not valid for use in specified state. -If when running the tool you get the error `Connect-MgGraph : Key not valid for use in specified state.`, this is due to a [bug](https://github.com/microsoftgraph/msgraph-sdk-powershell/issues/554) in the Microsoft Authentication Library. The workaround is to delete broken configuration information by running this command (replace `{username}` with your username): + + +### Microsoft Graph Errors + +#### Infinite AAD Signin Loop +While running the tool, AAD signin prompts sometimes get stuck in a loop. This is likely an issue with the connection to Microsoft Graph. + +To fix the loop, run: +```PowerShell +Disconnect-MgGraph +``` +Then run the tool again. + +#### Error `Connect-MgGraph : Key not valid for use in specified state.` + +This is due to a [bug](https://github.com/microsoftgraph/msgraph-sdk-powershell/issues/554) in the Microsoft Authentication Library. The workaround is to delete broken configuration information by running this command (replace `{username}` with your username): ``` rm -r C:\Users\{username}\.graph ``` - After deleting the `.graph` folder in your home directory, re-run the tool and the error should disappear. + +#### Error `Could not load file or assembly 'Microsoft.Graph.Authentication'` + +This indicates that the authentication module is at a version level that conflicts with the MS Graph modules used by the tool. Follow the instructions in the Installation section and execute the Setup script again. This will ensure that the module versions get synchronized with dependencies and then execute the tool again. + + +### Running the Script Behind Some Proxies +If you receive connection or network proxy errors, try running: +```powershell +$Wcl=New-Object System.Net.WebClient +$Wcl.Proxy.Credentials=[System.Net.CredentialCache]::DefaultNetworkCredentials +``` + +### Utility Scripts +The ScubaGear repository includes several utility scripts to help with troubleshooting and recovery from error conditions in the `utils` folder. These helper scripts are designed to assist developers and users when running into errors with the ScubaGear tool or local system environment. See the sections below for details on each script. + +#### ScubaGear Support +If a user receives errors and needs additional support diagnosing issues, the `ScubaGearSupport.ps1` script can be run to gather information about their system environment and previous tool output. +The script gathers this information into a single ZIP formatted archive to allow for easy sharing with developers or other support staff to assist in troubleshooting. Since the script does gather report output, do keep in mind that the resulting archive may contain details about the associated M365 environment and its settings. + +The script can be run with no arguments and will only collect environment information for troubleshooting. If the `IncludeReports` parameter is provided, it will contain the most recent report from the default `Reports` folder. + +```PowerShell +.\ScubaGearSupport.ps1 +``` + +An alternate report path can be specified via the `ReportPath` parameter. + +```PowerShell +.\ScubaGearSupport.ps1 -ReportPath C:\ScubaGear\Reports +``` + +Finally, the script can optionally include all previous reports rather than the most recent one by adding the `AllReports` option. + +```PowerShell +.\ScubaGearSupport.ps1 -AllReports +``` + +Data gathered by the script includes: +* Listings of locally installed PowerShell modules and their installation paths +* PowerShell versions and environment details +* WinRM client service Basic Authentication registry setting +* (optional) ScubaGear output from one or more previous invocations which contains + * HTML product and summary reports + * JSON-formatted M365 product configuration extracts + * JSON and CSV-formatted M365 baseline test results + +#### Removing installed modules +ScubaGear requires a number of PowerShell modules to function. A user or developer, however, may wish to remove these PowerShell modules for testing or for cleanup after ScubaGear has been run. The `UninstallModules.ps1` script will remove the latest version of the modules required by ScubaGear and installed by the associated `Setup.ps1` script. The script does not take any options and can be as follows: + +```PowerShell +.\UninstallModules.ps1 +``` + +>PowerShellGet 2.x has a known issue uninstalling modules installed on a OneDrive path that may result in an "Access to the cloud file is denied" error. Installing PSGet 3.0, currently in beta, will allow the script to successfully uninstall such modules or you can remove the modules files from OneDrive manually. + + diff --git a/Rego/AADConfig.rego b/Rego/AADConfig.rego index 825efccdd7..7b680bfefd 100644 --- a/Rego/AADConfig.rego +++ b/Rego/AADConfig.rego @@ -211,10 +211,10 @@ tests[{ "Criticality" : "Shall", "Commandlet" : "Get-MgIdentityConditionalAccessPolicy", "ActualValue" : Policies2_4_1, - "ReportDetails" : ReportDetailsArray(Policies2_4_1, DescriptionString), + "ReportDetails" : ReportFullDetailsArray(Policies2_4_1, DescriptionString), "RequirementMet" : count(Policies2_4_1) > 0 }]{ - DescriptionString := "conditional access policy(s) found that meet(s) all requirements.
Note: Policy exclusions and additional policy conditions may still limit a policy's scope more narrowly than desired. Recommend reviewing matching policies against the baseline statement to ensure a match between intent and implementation." + DescriptionString := "conditional access policy(s) found that meet(s) all requirements" true } #-- @@ -880,4 +880,4 @@ tests[{ ReportDetail := concat("", ["Permission level set to \"", LevelAsString(ExtractedRoleId), "\""]) Status := ExtractedRoleId in ["10dae51f-b6af-4016-8d66-8c2a99b929b3", "2af84b1e-32c8-42b7-82bc-daa82404023b"] } -#-- \ No newline at end of file +#-- diff --git a/Rego/DefenderConfig.rego b/Rego/DefenderConfig.rego index 57c0fefe5d..0ad24722ab 100644 --- a/Rego/DefenderConfig.rego +++ b/Rego/DefenderConfig.rego @@ -61,7 +61,7 @@ ApplyLicenseWarning(Message) := concat("", [ReportDetails(false), LicenseWarning # If a defender license is not present, assume failure and # replace the message with the warning input.defender_license == false - LicenseWarning := " **NOTE: Your tenant appears to not have a license for Defender for Microsoft Defender for Office 365, which is required for this feature.**" + LicenseWarning := " **NOTE: Either you do not have sufficient permissions or your tenant does not have a license for Microsoft Defender for Office 365 Plan 1, which is required for this feature.**" } ################ @@ -80,7 +80,7 @@ tests[{ "Requirement" : "Standard Preset security profiles SHOULD NOT be used", "Control" : "Defender 2.1", "Criticality" : "Should", - "Commandlet" : "Get-EOPProtectionPolicyRule", + "Commandlet" : ["Get-EOPProtectionPolicyRule"], "ActualValue" : Policy, "ReportDetails" : CustomizeError(ReportDetails(Status), ErrorMessage), "RequirementMet" : Status @@ -104,7 +104,7 @@ tests[{ "Requirement" : "Strict Preset security profiles SHOULD NOT be used", "Control" : "Defender 2.1", "Criticality" : "Should", - "Commandlet" : "Get-EOPProtectionPolicyRule", + "Commandlet" : ["Get-EOPProtectionPolicyRule"], "ActualValue" : Policy, "ReportDetails" : CustomizeError(ReportDetails(Status), ErrorMessage), "RequirementMet" : Status @@ -164,7 +164,7 @@ tests[{ "Requirement" : "A custom policy SHALL be configured to protect PII and sensitive information, as defined by the agency: U.S. Social Security Number (SSN)", "Control" : "Defender 2.2", "Criticality" : "Shall", - "Commandlet" : "Get-DLPComplianceRule", + "Commandlet" : ["Get-DlpComplianceRule"], "ActualValue" : Rules, "ReportDetails" : CustomizeError(ReportDetails(Status), ErrorMessage), "RequirementMet" : Status @@ -178,7 +178,7 @@ tests[{ "Requirement" : "A custom policy SHALL be configured to protect PII and sensitive information, as defined by the agency: U.S. Individual Taxpayer Identification Number (ITIN)", "Control" : "Defender 2.2", "Criticality" : "Shall", - "Commandlet" : "Get-DLPComplianceRule", + "Commandlet" : ["Get-DlpComplianceRule"], "ActualValue" : Rules, "ReportDetails" : CustomizeError(ReportDetails(Status), ErrorMessage), "RequirementMet" : Status @@ -192,7 +192,7 @@ tests[{ "Requirement" : "A custom policy SHALL be configured to protect PII and sensitive information, as defined by the agency: Credit Card Number", "Control" : "Defender 2.2", "Criticality" : "Shall", - "Commandlet" : "Get-DLPComplianceRule", + "Commandlet" : ["Get-DlpComplianceRule"], "ActualValue" : Rules, "ReportDetails" : CustomizeError(ReportDetails(Status), ErrorMessage), "RequirementMet" : Status @@ -259,7 +259,7 @@ tests[{ "Requirement" : "The custom policy SHOULD be applied in Exchange", "Control" : "Defender 2.2", "Criticality" : "Should", - "Commandlet" : "Get-DLPCompliancePolicy", + "Commandlet" : ["Get-DLPCompliancePolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), ErrorMessage), "RequirementMet" : Status @@ -273,7 +273,7 @@ tests[{ "Requirement" : "The custom policy SHOULD be applied in SharePoint", "Control" : "Defender 2.2", "Criticality" : "Should", - "Commandlet" : "Get-DLPCompliancePolicy", + "Commandlet" : ["Get-DLPCompliancePolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), ErrorMessage), "RequirementMet" : Status @@ -287,7 +287,7 @@ tests[{ "Requirement" : "The custom policy SHOULD be applied in OneDrive", "Control" : "Defender 2.2", "Criticality" : "Should", - "Commandlet" : "Get-DLPCompliancePolicy", + "Commandlet" : ["Get-DLPCompliancePolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), ErrorMessage), "RequirementMet" : Status @@ -301,7 +301,7 @@ tests[{ "Requirement" : "The custom policy SHOULD be applied in Teams", "Control" : "Defender 2.2", "Criticality" : "Should", - "Commandlet" : "Get-DLPCompliancePolicy", + "Commandlet" : ["Get-DLPCompliancePolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), ErrorMessage), "RequirementMet" : Status @@ -325,7 +325,7 @@ tests[{ "Requirement" : "The action for the DLP policy SHOULD be set to block sharing sensitive information with everyone when DLP conditions are met", "Control" : "Defender 2.2", "Criticality" : "Should", - "Commandlet" : "get-DLPComplianceRule", + "Commandlet" : ["Get-DlpComplianceRule"], "ActualValue" : Rules, "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Rules, ErrorMessage)), "RequirementMet" : Status @@ -349,7 +349,7 @@ tests[{ "Requirement" : "Notifications to inform users and help educate them on the proper use of sensitive information SHOULD be enabled", "Control" : "Defender 2.2", "Criticality" : "Should", - "Commandlet" : "get-DLPComplianceRule", + "Commandlet" : ["Get-DlpComplianceRule"], "ActualValue" : Rules, "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Rules, ErrorMessage)), "RequirementMet" : Status @@ -368,7 +368,7 @@ tests[{ "Requirement" : "A list of apps that are not allowed to access files protected by DLP policy SHOULD be defined", "Control" : "Defender 2.2", "Criticality" : "Should/Not-Implemented", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Currently cannot be checked automatically. See Defender Secure Configuration Baseline policy 2.2 for instructions on manual check", "RequirementMet" : false @@ -385,7 +385,7 @@ tests[{ "Requirement" : "A list of browsers that are not allowed to access files protected by DLP policy SHOULD be defined", "Control" : "Defender 2.2", "Criticality" : "Should/Not-Implemented", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Currently cannot be checked automatically. See Defender Secure Configuration Baseline policy 2.2 for instructions on manual check", "RequirementMet" : false @@ -411,7 +411,7 @@ tests[{ "Requirement" : "The common attachments filter SHALL be enabled in the default anti-malware policy and in all existing policies", "Control" : "Defender 2.3", "Criticality" : "Shall", - "Commandlet" : "Get-MalwareFilterPolicy", + "Commandlet" : ["Get-MalwareFilterPolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), "RequirementMet" : Status @@ -436,7 +436,7 @@ tests[{ "Requirement" : "Disallowed file types SHALL be determined and set. At a minimum, click-to-run files SHOULD be blocked: exe files", "Control" : "Defender 2.3", "Criticality" : "Should", - "Commandlet" : "Get-MalwareFilterPolicy", + "Commandlet" : ["Get-MalwareFilterPolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), ErrorMessage), "RequirementMet" : Status @@ -457,7 +457,7 @@ tests[{ "Requirement" : "Disallowed file types SHALL be determined and set. At a minimum, click-to-run files SHOULD be blocked: cmd files", "Control" : "Defender 2.3", "Criticality" : "Should", - "Commandlet" : "Get-MalwareFilterPolicy", + "Commandlet" : ["Get-MalwareFilterPolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), ErrorMessage), "RequirementMet" : Status @@ -478,7 +478,7 @@ tests[{ "Requirement" : "Disallowed file types SHALL be determined and set. At a minimum, click-to-run files SHOULD be blocked: vbe files", "Control" : "Defender 2.3", "Criticality" : "Should", - "Commandlet" : "Get-MalwareFilterPolicy", + "Commandlet" : ["Get-MalwareFilterPolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), ErrorMessage), "RequirementMet" : Status @@ -506,7 +506,7 @@ tests[{ "Requirement" : "Zero-hour Auto Purge (ZAP) for malware SHOULD be enabled in the default anti-malware policy and in all existing custom policies", "Control" : "Defender 2.4", "Criticality" : "Should", - "Commandlet" : "Get-MalwareFilterPolicy", + "Commandlet" : ["Get-MalwareFilterPolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), "RequirementMet" : Status @@ -541,7 +541,7 @@ tests[{ "Requirement" : "User impersonation protection SHOULD be enabled for key agency leaders", "Control" : "Defender 2.5", "Criticality" : "Should", - "Commandlet" : "Get-AntiPhishPolicy", + "Commandlet" : ["Get-AntiPhishPolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), ErrorMessage), "RequirementMet" : Status @@ -572,7 +572,7 @@ tests[{ "Requirement" : "Domain impersonation protection SHOULD be enabled for domains owned by the agency", "Control" : "Defender 2.5", "Criticality" : "Should", - "Commandlet" : "Get-AntiPhishPolicy", + "Commandlet" : ["Get-AntiPhishPolicy"], "ActualValue" : Policies, "ReportDetails" : ReportDetails(Status), "RequirementMet" : Status @@ -602,7 +602,7 @@ tests[{ "Requirement" : "Domain impersonation protection SHOULD be added for frequent partners", "Control" : "Defender 2.5", "Criticality" : "Should", - "Commandlet" : "Get-AntiPhishPolicy", + "Commandlet" : ["Get-AntiPhishPolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), ErrorMessage), "RequirementMet" : Status @@ -632,7 +632,7 @@ tests[{ "Requirement" : "Intelligence for impersonation protection SHALL be enabled", "Control" : "Defender 2.5", "Criticality" : "Should", - "Commandlet" : "Get-AntiPhishPolicy", + "Commandlet" : ["Get-AntiPhishPolicy"], "ActualValue" : Policies, "ReportDetails" : ReportDetails(Status), "RequirementMet" : Status @@ -650,7 +650,7 @@ tests[{ "Requirement" : "Message action SHALL be set to quarantine if the message is detected as impersonated: users default policy", "Control" : "Defender 2.5", "Criticality" : "Shall", - "Commandlet" : "Get-AntiPhishPolicy", + "Commandlet" : ["Get-AntiPhishPolicy"], "ActualValue" : Policy.TargetedUserProtectionAction, "ReportDetails" : ReportDetails(Status), "RequirementMet" : Status @@ -664,7 +664,7 @@ tests[{ "Requirement" : "Message action SHALL be set to quarantine if the message is detected as impersonated: domains default policy", "Control" : "Defender 2.5", "Criticality" : "Shall", - "Commandlet" : "Get-AntiPhishPolicy", + "Commandlet" : ["Get-AntiPhishPolicy"], "ActualValue" : Policy.TargetedDomainProtectionAction, "ReportDetails" : ReportDetails(Status), "RequirementMet" : Status @@ -678,7 +678,7 @@ tests[{ "Requirement" : "Message action SHALL be set to quarantine if the message is detected as impersonated: mailbox default policy", "Control" : "Defender 2.5", "Criticality" : "Shall", - "Commandlet" : "Get-AntiPhishPolicy", + "Commandlet" : ["Get-AntiPhishPolicy"], "ActualValue" : Policy.MailboxIntelligenceProtectionAction, "ReportDetails" : ReportDetails(Status), "RequirementMet" : Status @@ -702,7 +702,7 @@ tests[ { "Requirement" : "Message action SHOULD be set to quarantine if the message is detected as impersonated: users non-default policies", "Control" : "Defender 2.5", "Criticality" : "Should", - "Commandlet" : "Get-AntiPhishPolicy", + "Commandlet" : ["Get-AntiPhishPolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), "RequirementMet" : Status @@ -724,7 +724,7 @@ tests[ { "Requirement" : "Message action SHOULD be set to quarantine if the message is detected as impersonated: domains non-default policies", "Control" : "Defender 2.5", "Criticality" : "Should", - "Commandlet" : "Get-AntiPhishPolicy", + "Commandlet" : ["Get-AntiPhishPolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), "RequirementMet" : Status @@ -746,7 +746,7 @@ tests[ { "Requirement" : "Message action SHOULD be set to quarantine if the message is detected as impersonated: mailbox non-default policies", "Control" : "Defender 2.5", "Criticality" : "Should", - "Commandlet" : "Get-AntiPhishPolicy", + "Commandlet" : ["Get-AntiPhishPolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), "RequirementMet" : Status @@ -767,7 +767,7 @@ tests[ { "Requirement" : "Mail classified as spoofed SHALL be quarantined: default policy", "Control" : "Defender 2.5", "Criticality" : "Shall", - "Commandlet" : "Get-AntiPhishPolicy", + "Commandlet" : ["Get-AntiPhishPolicy"], "ActualValue" : Policy.AuthenticationFailAction, "ReportDetails" : ReportDetails(Status), "RequirementMet" : Status @@ -791,7 +791,7 @@ tests[ { "Requirement" : "Mail classified as spoofed SHOULD be quarantined: non-default policies", "Control" : "Defender 2.5", "Criticality" : "Should", - "Commandlet" : "Get-AntiPhishPolicy", + "Commandlet" : ["Get-AntiPhishPolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), "RequirementMet" : Status @@ -810,7 +810,7 @@ tests[ { "Requirement" : "All safety tips SHALL be enabled: first contact default policy", "Control" : "Defender 2.5", "Criticality" : "Shall", - "Commandlet" : "Get-AntiPhishPolicy", + "Commandlet" : ["Get-AntiPhishPolicy"], "ActualValue" : Policy.EnableFirstContactSafetyTips, "ReportDetails" : ReportDetails(Status), "RequirementMet" : Status @@ -833,7 +833,7 @@ tests[{ "Requirement" : "All safety tips SHOULD be enabled: first contact non-default policies", "Control" : "Defender 2.5", "Criticality" : "Should", - "Commandlet" : "Get-AntiPhishPolicy", + "Commandlet" : ["Get-AntiPhishPolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), "RequirementMet" : Status @@ -848,7 +848,7 @@ tests[{ "Requirement" : "All safety tips SHALL be enabled: user impersonation default policy", "Control" : "Defender 2.5", "Criticality" : "Shall", - "Commandlet" : "Get-AntiPhishPolicy", + "Commandlet" : ["Get-AntiPhishPolicy"], "ActualValue" : Policy.EnableSimilarUsersSafetyTips, "ReportDetails" : ReportDetails(Status), "RequirementMet" : Status @@ -871,7 +871,7 @@ tests[{ "Requirement" : "All safety tips SHOULD be enabled: user impersonation non-default policies", "Control" : "Defender 2.5", "Criticality" : "Should", - "Commandlet" : "Get-AntiPhishPolicy", + "Commandlet" : ["Get-AntiPhishPolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), "RequirementMet" : Status @@ -886,7 +886,7 @@ tests[{ "Requirement" : "All safety tips SHALL be enabled: domain impersonation default policy", "Control" : "Defender 2.5", "Criticality" : "Shall", - "Commandlet" : "Get-AntiPhishPolicy", + "Commandlet" : ["Get-AntiPhishPolicy"], "ActualValue" : Policy.EnableSimilarDomainsSafetyTips, "ReportDetails" : ReportDetails(Status), "RequirementMet" : Status @@ -909,7 +909,7 @@ tests[{ "Requirement" : "All safety tips SHOULD be enabled: domain impersonation non-default policies", "Control" : "Defender 2.5", "Criticality" : "Should", - "Commandlet" : "Get-AntiPhishPolicy", + "Commandlet" : ["Get-AntiPhishPolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), "RequirementMet" : Status @@ -924,7 +924,7 @@ tests[{ "Requirement" : "All safety tips SHALL be enabled: user impersonation unusual characters default policy", "Control" : "Defender 2.5", "Criticality" : "Shall", - "Commandlet" : "Get-AntiPhishPolicy", + "Commandlet" : ["Get-AntiPhishPolicy"], "ActualValue" : Policy.EnableUnusualCharactersSafetyTips, "ReportDetails" : ReportDetails(Status), "RequirementMet" : Status @@ -947,7 +947,7 @@ tests[{ "Requirement" : "All safety tips SHOULD be enabled: user impersonation unusual characters non-default policies", "Control" : "Defender 2.5", "Criticality" : "Should", - "Commandlet" : "Get-AntiPhishPolicy", + "Commandlet" : ["Get-AntiPhishPolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), "RequirementMet" : Status @@ -962,7 +962,7 @@ tests[{ "Requirement" : "All safety tips SHALL be enabled: \"via\" tag default policy", "Control" : "Defender 2.5", "Criticality" : "Shall", - "Commandlet" : "Get-AntiPhishPolicy", + "Commandlet" : ["Get-AntiPhishPolicy"], "ActualValue" : Policy.EnableViaTag, "ReportDetails" : ReportDetails(Status), "RequirementMet" : Status @@ -985,7 +985,7 @@ tests[{ "Requirement" : "All safety tips SHOULD be enabled: \"via\" tag non-default policies", "Control" : "Defender 2.5", "Criticality" : "Should", - "Commandlet" : "Get-AntiPhishPolicy", + "Commandlet" : ["Get-AntiPhishPolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), "RequirementMet" : Status @@ -1000,7 +1000,7 @@ tests[{ "Requirement" : "All safety tips SHALL be enabled: \"?\" for unauthenticated senders for spoof default policy", "Control" : "Defender 2.5", "Criticality" : "Shall", - "Commandlet" : "Get-AntiPhishPolicy", + "Commandlet" : ["Get-AntiPhishPolicy"], "ActualValue" : Policy.EnableUnauthenticatedSender, "ReportDetails" : ReportDetails(Status), "RequirementMet" : Status @@ -1023,7 +1023,7 @@ tests[{ "Requirement" : "All safety tips SHOULD be enabled: \"?\" for unauthenticated senders for spoof non-default policies", "Control" : "Defender 2.5", "Criticality" : "Should", - "Commandlet" : "Get-AntiPhishPolicy", + "Commandlet" : ["Get-AntiPhishPolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), "RequirementMet" : Status @@ -1047,7 +1047,7 @@ tests[{ "Requirement" : "The bulk complaint level (BCL) threshold SHOULD be set to six or lower: default policy", "Control" : "Defender 2.6", "Criticality" : "Should", - "Commandlet" : "Get-HostedContentFilterPolicy", + "Commandlet" : ["Get-HostedContentFilterPolicy"], "ActualValue" : Policy.BulkThreshold, "ReportDetails" : ReportDetails(Status), "RequirementMet" : Status @@ -1067,7 +1067,7 @@ tests[{ "Requirement" : "The bulk complaint level (BCL) threshold SHOULD be set to six or lower: non-default policies", "Control" : "Defender 2.6", "Criticality" : "Should", - "Commandlet" : "Get-HostedContentFilterPolicy", + "Commandlet" : ["Get-HostedContentFilterPolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), "RequirementMet" : Status @@ -1086,7 +1086,7 @@ tests[{ "Requirement" : "Spam SHALL be moved to either the junk email folder or the quarantine folder: default policy", "Control" : "Defender 2.6", "Criticality" : "Shall", - "Commandlet" : "Get-HostedContentFilterPolicy", + "Commandlet" : ["Get-HostedContentFilterPolicy"], "ActualValue" : Policy.SpamAction, "ReportDetails" : ReportDetails(Status), "RequirementMet" : Status @@ -1100,7 +1100,7 @@ tests[{ "Requirement" : "High confidence spam SHALL be moved to either the junk email folder or the quarantine folder: default policy", "Control" : "Defender 2.6", "Criticality" : "Shall", - "Commandlet" : "Get-HostedContentFilterPolicy", + "Commandlet" : ["Get-HostedContentFilterPolicy"], "ActualValue" : Policy.HighConfidenceSpamAction, "ReportDetails" : ReportDetails(Status), "RequirementMet" : Status @@ -1121,7 +1121,7 @@ tests[{ "Requirement" : "Spam SHOULD be moved to either the junk email folder or the quarantine folder: non-default policies", "Control" : "Defender 2.6", "Criticality" : "Should", - "Commandlet" : "Get-HostedContentFilterPolicy", + "Commandlet" : ["Get-HostedContentFilterPolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), "RequirementMet" : Status @@ -1141,7 +1141,7 @@ tests[{ "Requirement" : "High confidence spam SHOULD be moved to either the junk email folder or the quarantine folder: non-default policies", "Control" : "Defender 2.6", "Criticality" : "Should", - "Commandlet" : "Get-HostedContentFilterPolicy", + "Commandlet" : ["Get-HostedContentFilterPolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), "RequirementMet" : Status @@ -1160,7 +1160,7 @@ tests[{ "Requirement" : "Phishing SHALL be quarantined: default policy", "Control" : "Defender 2.6", "Criticality" : "Shall", - "Commandlet" : "Get-HostedContentFilterPolicy", + "Commandlet" : ["Get-HostedContentFilterPolicy"], "ActualValue" : Policy.PhishSpamAction, "ReportDetails" : ReportDetails(Status), "RequirementMet" : Status @@ -1174,7 +1174,7 @@ tests[{ "Requirement" : "High confidence phishing SHALL be quarantined: default policy", "Control" : "Defender 2.6", "Criticality" : "Shall", - "Commandlet" : "Get-HostedContentFilterPolicy", + "Commandlet" : ["Get-HostedContentFilterPolicy"], "ActualValue" : Policy.HighConfidencePhishAction, "ReportDetails" : ReportDetails(Status), "RequirementMet" : Status @@ -1196,7 +1196,7 @@ tests[{ "Requirement" : "Phishing SHOULD be quarantined: non-default policies", "Control" : "Defender 2.6", "Criticality" : "Should", - "Commandlet" : "Get-HostedContentFilterPolicy", + "Commandlet" : ["Get-HostedContentFilterPolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), "RequirementMet" : Status @@ -1216,7 +1216,7 @@ tests[{ "Requirement" : "High confidence phishing SHOULD be quarantined: non-default policies", "Control" : "Defender 2.6", "Criticality" : "Should", - "Commandlet" : "Get-HostedContentFilterPolicy", + "Commandlet" : ["Get-HostedContentFilterPolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), "RequirementMet" : Status @@ -1233,7 +1233,7 @@ tests[{ "Requirement" : "Bulk email SHOULD be moved to either the junk email folder or the quarantine folder: default policy", "Control" : "Defender 2.6", "Criticality" : "Should", - "Commandlet" : "Get-HostedContentFilterPolicy", + "Commandlet" : ["Get-HostedContentFilterPolicy"], "ActualValue" : Policy.BulkSpamAction, "ReportDetails" : ReportDetails(Status), "RequirementMet" : Status @@ -1253,7 +1253,7 @@ tests[{ "Requirement" : "Bulk email SHOULD be moved to either the junk email folder or the quarantine folder: non-default policies", "Control" : "Defender 2.6", "Criticality" : "Should", - "Commandlet" : "Get-HostedContentFilterPolicy", + "Commandlet" : ["Get-HostedContentFilterPolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), "RequirementMet" : Status @@ -1271,7 +1271,7 @@ tests[{ "Requirement" : "Spam in quarantine SHOULD be retained for at least 30 days: default policy", "Control" : "Defender 2.6", "Criticality" : "Should", - "Commandlet" : "Get-HostedContentFilterPolicy", + "Commandlet" : ["Get-HostedContentFilterPolicy"], "ActualValue" : Policy.QuarantineRetentionPeriod, "ReportDetails" : ReportDetails(Status), "RequirementMet" : Status @@ -1291,7 +1291,7 @@ tests[{ "Requirement" : "Spam in quarantine SHOULD be retained for at least 30 days: non-default policies", "Control" : "Defender 2.6", "Criticality" : "Should", - "Commandlet" : "Get-HostedContentFilterPolicy", + "Commandlet" : ["Get-HostedContentFilterPolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), "RequirementMet" : Status @@ -1309,7 +1309,7 @@ tests[{ "Requirement" : "Spam safety tips SHOULD be turned on: default policy", "Control" : "Defender 2.6", "Criticality" : "Should", - "Commandlet" : "Get-HostedContentFilterPolicy", + "Commandlet" : ["Get-HostedContentFilterPolicy"], "ActualValue" : Policy.InlineSafetyTipsEnabled, "ReportDetails" : ReportDetails(Status), "RequirementMet" : Status @@ -1329,7 +1329,7 @@ tests[{ "Requirement" : "Spam safety tips SHOULD be turned on: non-default policies", "Control" : "Defender 2.6", "Criticality" : "Should", - "Commandlet" : "Get-HostedContentFilterPolicy", + "Commandlet" : ["Get-HostedContentFilterPolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), "RequirementMet" : Status @@ -1348,7 +1348,7 @@ tests[{ "Requirement" : "Zero-hour auto purge (ZAP) SHALL be enabled: default policy", "Control" : "Defender 2.6", "Criticality" : "Shall", - "Commandlet" : "Get-HostedContentFilterPolicy", + "Commandlet" : ["Get-HostedContentFilterPolicy"], "ActualValue" : Policy.ZapEnabled, "ReportDetails" : ReportDetails(Status), "RequirementMet" : Status @@ -1362,7 +1362,7 @@ tests[{ "Requirement" : "Zero-hour auto purge (ZAP) SHALL be enabled for spam messages: default policy", "Control" : "Defender 2.6", "Criticality" : "Shall", - "Commandlet" : "Get-HostedContentFilterPolicy", + "Commandlet" : ["Get-HostedContentFilterPolicy"], "ActualValue" : Policy.SpamZapEnabled, "ReportDetails" : ReportDetails(Status), "RequirementMet" : Status @@ -1376,7 +1376,7 @@ tests[{ "Requirement" : "Zero-hour auto purge (ZAP) SHALL be enabled for phishing: default policy", "Control" : "Defender 2.6", "Criticality" : "Shall", - "Commandlet" : "Get-HostedContentFilterPolicy", + "Commandlet" : ["Get-HostedContentFilterPolicy"], "ActualValue" : Policy.PhishZapEnabled, "ReportDetails" : ReportDetails(Status), "RequirementMet" : Status @@ -1397,7 +1397,7 @@ tests[{ "Requirement" : "Zero-hour auto purge (ZAP) SHOULD be enabled: non-default", "Control" : "Defender 2.6", "Criticality" : "Should", - "Commandlet" : "Get-HostedContentFilterPolicy", + "Commandlet" : ["Get-HostedContentFilterPolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), "RequirementMet" : Status @@ -1417,7 +1417,7 @@ tests[{ "Requirement" : "Zero-hour auto purge (ZAP) SHOULD be enabled for Spam: non-default", "Control" : "Defender 2.6", "Criticality" : "Should", - "Commandlet" : "Get-HostedContentFilterPolicy", + "Commandlet" : ["Get-HostedContentFilterPolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), "RequirementMet" : Status @@ -1437,7 +1437,7 @@ tests[{ "Requirement" : "Zero-hour auto purge (ZAP) SHOULD be enabled for phishing: non-default", "Control" : "Defender 2.6", "Criticality" : "Should", - "Commandlet" : "Get-HostedContentFilterPolicy", + "Commandlet" : ["Get-HostedContentFilterPolicy"], "ActualValue" : Policies, "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), "RequirementMet" : Status @@ -1451,17 +1451,42 @@ tests[{ # # Baseline 2.6: Policy 8 #-- -# At this time we are unable to test for X because of Y +AllowedSenderDomainsNotEmpty [Policy.Identity] { + Policy := input.hosted_content_filter_policies[_] + Policy.Identity == "Default" + count(Policy.AllowedSenderDomains) > 0 +} tests[{ - "Requirement" : "Allowed senders MAY be added but allowed domains SHALL NOT be added", + "Requirement" : "Allowed senders MAY be added but allowed domains SHALL NOT be added: default policy", "Control" : "Defender 2.6", - "Criticality" : "Shall/Not-Implemented", - "Commandlet" : "", - "ActualValue" : [], - "ReportDetails" : "Currently cannot be checked automatically. See Defender Secure Configuration Baseline policy 2.8 for instructions on manual check", - "RequirementMet" : false + "Criticality" : "Shall", + "Commandlet" : ["Get-HostedContentFilterPolicy"], + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), + "RequirementMet" : Status }] { - true + ErrorMessage := "custom anti-spam policy(ies) found where there is at least one allowed sender domain:" + Policies = AllowedSenderDomainsNotEmpty + Status := count(Policies) == 0 +} + +AllowedSenderDomainsNotEmptyCustom [Policy.Identity] { + Policy := input.hosted_content_filter_policies[_] + Policy.Identity != "Default" + count(Policy.AllowedSenderDomains) > 0 +} +tests[{ + "Requirement" : "Allowed senders MAY be added but allowed domains SHOULD NOT be added: non-default", + "Control" : "Defender 2.6", + "Criticality" : "Should", + "Commandlet" : ["Get-HostedContentFilterPolicy"], + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), + "RequirementMet" : Status +}] { + ErrorMessage := "custom policy(ies) found where there is at least one allowed sender domain:" + Policies = AllowedSenderDomainsNotEmptyCustom + Status := count(Policies) == 0 } #-- @@ -1474,7 +1499,7 @@ tests[{ # Baseline 2.7: Policy 1 #-- AllDomainsSafeLinksPolicies[{ - "Identity" : Rule.Identity, + "Identity" : Rule.SafeLinksPolicy, "RecipientDomains" : RecipientDomains}] { Rule := input.safe_links_rules[_] Rule.State == "Enabled" @@ -1488,7 +1513,7 @@ tests[{ "Requirement" : "The Safe Links Policy SHALL include all agency domains-and by extension-all users", "Control" : "Defender 2.7", "Criticality" : "Shall", - "Commandlet" : "Get-SafeLinksPolicy", + "Commandlet" : ["Get-SafeLinksRule", "Get-AcceptedDomain"], "ActualValue" : AllDomainsSafeLinksPolicies, "ReportDetails" : ApplyLicenseWarning(CustomizeError(ReportDetails(Status), ErrorMessage)), "RequirementMet" : Status @@ -1507,7 +1532,7 @@ EnableSafeLinksForEmailCorrect[Policy.Identity] { Policy.Identity != "Built-In Protection Policy" Policy.EnableSafeLinksForEmail == true Rule := input.safe_links_rules[_] - Rule.Identity == Policy.Identity + Rule.SafeLinksPolicy == Policy.Identity Rule.State == "Enabled" } @@ -1515,7 +1540,7 @@ tests[{ "Requirement" : "URL rewriting and malicious link click checking SHALL be enabled", "Control" : "Defender 2.7", "Criticality" : "Shall", - "Commandlet" : "Get-SafeLinksPolicy", + "Commandlet" : ["Get-SafeLinksPolicy", "Get-SafeLinksRule"], "ActualValue" : Policies, "ReportDetails" : ApplyLicenseWarning(ReportDetails(Status)), "RequirementMet" : Status @@ -1533,7 +1558,7 @@ EnableSafeLinksForTeamsCorrect[Policy.Identity] { Policy.Identity != "Built-In Protection Policy" Policy.EnableSafeLinksForTeams == true Rule := input.safe_links_rules[_] - Rule.Identity == Policy.Identity + Rule.SafeLinksPolicy == Policy.Identity Rule.State == "Enabled" } @@ -1541,7 +1566,7 @@ tests[{ "Requirement" : "Malicious link click checking SHALL be enabled with Microsoft Teams", "Control" : "Defender 2.7", "Criticality" : "Shall", - "Commandlet" : "Get-SafeLinksPolicy", + "Commandlet" : ["Get-SafeLinksPolicy", "Get-SafeLinksRule"], "ActualValue" : Policies, "ReportDetails" : ApplyLicenseWarning(ReportDetails(Status)), "RequirementMet" : Status @@ -1559,7 +1584,7 @@ ScanUrlsCorrect[Policy.Identity] { Policy.Identity != "Built-In Protection Policy" Policy.ScanUrls == true Rule := input.safe_links_rules[_] - Rule.Identity == Policy.Identity + Rule.SafeLinksPolicy == Policy.Identity Rule.State == "Enabled" } @@ -1567,7 +1592,7 @@ tests[{ "Requirement" : "Real-time suspicious URL and file-link scanning SHALL be enabled", "Control" : "Defender 2.7", "Criticality" : "Shall", - "Commandlet" : "Get-SafeLinksPolicy", + "Commandlet" : ["Get-SafeLinksPolicy", "Get-SafeLinksRule"], "ActualValue" : Policies, "ReportDetails" : ApplyLicenseWarning(ReportDetails(Status)), "RequirementMet" : Status @@ -1585,7 +1610,7 @@ DeliverMessageAfterScanCorrect[Policy.Identity] { Policy.Identity != "Built-In Protection Policy" Policy.DeliverMessageAfterScan == true Rule := input.safe_links_rules[_] - Rule.Identity == Policy.Identity + Rule.SafeLinksPolicy == Policy.Identity Rule.State == "Enabled" } @@ -1593,7 +1618,7 @@ tests[{ "Requirement" : "URLs SHALL be scanned completely before message delivery", "Control" : "Defender 2.7", "Criticality" : "Shall", - "Commandlet" : "Get-SafeLinksPolicy", + "Commandlet" : ["Get-SafeLinksPolicy", "Get-SafeLinksRule"], "ActualValue" : Policies, "ReportDetails" : ApplyLicenseWarning(ReportDetails(Status)), "RequirementMet" : Status @@ -1611,7 +1636,7 @@ EnableForInternalSendersCorrect[Policy.Identity] { Policy.Identity != "Built-In Protection Policy" Policy.EnableForInternalSenders == true Rule := input.safe_links_rules[_] - Rule.Identity == Policy.Identity + Rule.SafeLinksPolicy == Policy.Identity Rule.State == "Enabled" } @@ -1619,7 +1644,7 @@ tests[{ "Requirement" : "Internal agency email messages SHALL have safe links enabled", "Control" : "Defender 2.7", "Criticality" : "Shall", - "Commandlet" : "Get-SafeLinksPolicy", + "Commandlet" : ["Get-SafeLinksPolicy", "Get-SafeLinksRule"], "ActualValue" : Policies, "ReportDetails" : ApplyLicenseWarning(ReportDetails(Status)), "RequirementMet" : Status @@ -1637,7 +1662,7 @@ TrackClicksCorrect[Policy.Identity] { Policy.Identity != "Built-In Protection Policy" Policy.TrackClicks == true Rule := input.safe_links_rules[_] - Rule.Identity == Policy.Identity + Rule.SafeLinksPolicy == Policy.Identity Rule.State == "Enabled" } @@ -1645,7 +1670,7 @@ tests[{ "Requirement" : "User click tracking SHALL be enabled", "Control" : "Defender 2.7", "Criticality" : "Shall", - "Commandlet" : "Get-SafeLinksPolicy", + "Commandlet" : ["Get-SafeLinksPolicy", "Get-SafeLinksRule"], "ActualValue" : Policies, "ReportDetails" : ApplyLicenseWarning(ReportDetails(Status)), "RequirementMet" : Status @@ -1662,7 +1687,7 @@ EnableSafeLinksForOfficeCorrect[Policy.Identity] { Policy := input.safe_links_policies[_] Policy.EnableSafeLinksForOffice == true Rule := input.safe_links_rules[_] - Rule.Identity == Policy.Identity + Rule.SafeLinksPolicy == Policy.Identity Rule.State == "Enabled" } @@ -1670,7 +1695,7 @@ tests[{ "Requirement" : "Safe Links in Office 365 apps SHALL be turned on", "Control" : "Defender 2.7", "Criticality" : "Shall", - "Commandlet" : "Get-SafeLinksPolicy", + "Commandlet" : ["Get-SafeLinksPolicy", "Get-SafeLinksRule"], "ActualValue" : Policies, "ReportDetails" : ApplyLicenseWarning(ReportDetails(Status)), "RequirementMet" : Status @@ -1687,7 +1712,7 @@ AllowClickThroughCorrect[Policy.Identity] { Policy := input.safe_links_policies[_] Policy.AllowClickThrough == false Rule := input.safe_links_rules[_] - Rule.Identity == Policy.Identity + Rule.SafeLinksPolicy == Policy.Identity Rule.State == "Enabled" } @@ -1695,7 +1720,7 @@ tests[{ "Requirement" : "Users SHALL NOT be enabled to click through to the original URL", "Control" : "Defender 2.7", "Criticality" : "Shall", - "Commandlet" : "Get-SafeLinksPolicy", + "Commandlet" : ["Get-SafeLinksPolicy", "Get-SafeLinksRule"], "ActualValue" : Policies, "ReportDetails" : ApplyLicenseWarning(ReportDetails(Status)), "RequirementMet" : Status @@ -1715,7 +1740,7 @@ tests[{ #-- # find the set of policies that are applied to all of the tenant's domains AllDomainsSafeAttachmentRules[{ - "Identity" : Rule.Identity, + "SafeAttachmentPolicy" : Rule.SafeAttachmentPolicy, "RecipientDomains" : RecipientDomains}] { Rule := input.safe_attachment_rules[_] DomainNames = {Name.DomainName | Name = input.all_domains[_]} @@ -1728,7 +1753,7 @@ tests[{ "Requirement" : "At least one Safe Attachments Policy SHALL include all agency domains-and by extension-all users", "Control" : "Defender 2.8", "Criticality" : "Shall", - "Commandlet" : "Get-SafeAttachmentRule", + "Commandlet" : ["Get-SafeAttachmentRule", "Get-AcceptedDomain"], "ActualValue" : AllDomainsSafeAttachmentRules, "ReportDetails" : ApplyLicenseWarning(CustomizeError(ReportDetails(Status), ErrorMessage)), "RequirementMet" : Status @@ -1754,7 +1779,7 @@ BlockMalwarePolicies[{ SafeAttachmentPolicies := input.safe_attachment_policies[_] SafeAttachmentPolicies.Action == "Block" SafeAttachmentPolicies.Enable - AllDomainsPoliciesNames := {Rule.Identity | Rule = AllDomainsSafeAttachmentRules[_]} + AllDomainsPoliciesNames := {Rule.SafeAttachmentPolicy | Rule = AllDomainsSafeAttachmentRules[_]} SafeAttachmentPolicies.Identity in AllDomainsPoliciesNames } @@ -1762,7 +1787,7 @@ tests[{ "Requirement" : "The action for malware in email attachments SHALL be set to block", "Control" : "Defender 2.8", "Criticality" : "Shall", - "Commandlet" : "Get-SafeAttachmentPolicy", + "Commandlet" : ["Get-SafeAttachmentPolicy", "Get-SafeAttachmentRule", "Get-AcceptedDomain"], "ActualValue" : Policies, "ReportDetails" : ApplyLicenseWarning(CustomizeError(ReportDetails(Status), ErrorMessage)), "RequirementMet" : Status @@ -1789,7 +1814,7 @@ tests[{ "Requirement" : "Redirect emails with detected attachments to an agency-specified email SHOULD be enabled", "Control" : "Defender 2.8", "Criticality" : "Should", - "Commandlet" : "Get-SafeAttachmentPolicy", + "Commandlet" : ["Get-SafeAttachmentPolicy", "Get-SafeAttachmentRule", "Get-AcceptedDomain"], "ActualValue" : Policies, "ReportDetails" : ApplyLicenseWarning(CustomizeError(ReportDetails(Status), ErrorMessage)), "RequirementMet" : Status @@ -1815,7 +1840,7 @@ tests[{ "Requirement" : "Safe attachments SHOULD be enabled for SharePoint, OneDrive, and Microsoft Teams", "Control" : "Defender 2.8", "Criticality" : "Should", - "Commandlet" : "Get-AtpPolicyForO365", + "Commandlet" : ["Get-AtpPolicyForO365"], "ActualValue" : Policies, "ReportDetails" : ApplyLicenseWarning(ReportDetails(Status)), "RequirementMet" : Status @@ -1855,7 +1880,7 @@ tests[{ "Requirement" : "At a minimum, the alerts required by the Exchange Online Minimum Viable Secure Configuration Baseline SHALL be enabled", "Control" : "Defender 2.9", "Criticality" : "Shall", - "Commandlet" : "Get-ProtectionAlert", + "Commandlet" : ["Get-ProtectionAlert"], "ActualValue" : MissingAlerts, "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(MissingAlerts, ErrorMessage)), "RequirementMet" : Status @@ -1874,7 +1899,7 @@ tests[{ "Requirement" : "The alerts SHOULD be sent to a monitored address or incorporated into a SIEM", "Control" : "Defender 2.9", "Criticality" : "Should/Not-Implemented", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Currently cannot be checked automatically. See Defender Secure Configuration Baseline policy 2.9 for instructions on manual check", "RequirementMet" : false @@ -1888,6 +1913,14 @@ tests[{ # Baseline 2.10 # ################# +CorrectLogConfigs[{ + "Identity": AuditLog.Identity, + "UnifiedAuditLogIngestionEnabled": AuditLog.UnifiedAuditLogIngestionEnabled +}] { + AuditLog := input.admin_audit_log_config[_] + AuditLog.UnifiedAuditLogIngestionEnabled == true +} + # # Baseline 2.10: Policy 1 #-- @@ -1895,13 +1928,12 @@ tests[{ "Requirement" : "Unified audit logging SHALL be enabled", "Control" : "Defender 2.10", "Criticality" : "Shall", - "Commandlet" : "Get-AdminAuditLogConfig", - "ActualValue" : AuditLog.UnifiedAuditLogIngestionEnabled, + "Commandlet" : ["Get-AdminAuditLogConfig"], + "ActualValue" : CorrectLogConfigs, "ReportDetails" : ReportDetails(Status), "RequirementMet" : Status }] { - AuditLog := input.admin_audit_log_config - Status := AuditLog.UnifiedAuditLogIngestionEnabled == true + Status := count(CorrectLogConfigs) >= 1 } #-- @@ -1915,7 +1947,7 @@ tests[{ "Requirement" : "Advanced audit SHALL be enabled", "Control" : "Defender 2.10", "Criticality" : "Shall/Not-Implemented", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Currently cannot be checked automatically. See Defender Secure Configuration Baseline policy 2.10 for instructions on manual check", "RequirementMet" : false @@ -1929,16 +1961,16 @@ tests[{ #-- # Dictated by OMB M-21-31: 12 months in hot storage and 18 months in cold # It is not required to maintain these logs in the M365 cloud environment; doing so would require an additional add-on SKU. -#This requirement can be met by offloading the logs out of the cloud environment. +# This requirement can be met by offloading the logs out of the cloud environment. tests[{ "Requirement" : "Audit logs SHALL be maintained for at least the minimum duration dictated by OMB M-21-31", "Control" : "Defender 2.10", "Criticality" : "Shall/Not-Implemented", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Currently cannot be checked automatically. See Defender Secure Configuration Baseline policy 2.10 for instructions on manual check", "RequirementMet" : false }] { true } -#-- \ No newline at end of file +#-- diff --git a/Rego/EXOConfig.rego b/Rego/EXOConfig.rego index 1d884a7bd1..4918f6aed6 100644 --- a/Rego/EXOConfig.rego +++ b/Rego/EXOConfig.rego @@ -55,7 +55,7 @@ tests[{ "Requirement" : "Automatic forwarding to external domains SHALL be disabled", "Control" : "EXO 2.1", "Criticality" : "Shall", - "Commandlet" : "Get-RemoteDomain", + "Commandlet" : ["Get-RemoteDomain"], "ActualValue" : Domains, "ReportDetails" : ReportDetailsString(Status, ErrorMessage), "RequirementMet" : Status @@ -79,7 +79,7 @@ tests[{ "Requirement" : "A list of approved IP addresses for sending mail SHALL be maintained", "Control" : "EXO 2.2", "Criticality" : "Shall/Not-Implemented", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Currently cannot be checked automatically. See Exchange Online Secure Configuration Baseline policy 2.# for instructions on manual check", "RequirementMet" : false @@ -101,7 +101,7 @@ tests[{ "Requirement" : "An SPF policy(s) that designates only these addresses as approved senders SHALL be published", "Control" : "EXO 2.2", "Criticality" : "Shall", - "Commandlet" : "Resolve-DnsName", + "Commandlet" : ["Get-ScubaSpfRecords", "Get-AcceptedDomain"], "ActualValue" : Domains, "ReportDetails" : ReportDetailsArray(Status, Domains, AllDomains), "RequirementMet" : Status @@ -131,7 +131,7 @@ tests[{ "Requirement" : "DKIM SHOULD be enabled for any custom domain", "Control" : "EXO 2.3", "Criticality" : "Should", - "Commandlet" : "Get-DkimSigningConfig, Resolve-DnsName", + "Commandlet" : ["Get-DkimSigningConfig", "Get-ScubaDkimRecords", "Get-AcceptedDomain"], "ActualValue" : [input.dkim_records, input.dkim_config], "ReportDetails" : ReportDetailsArray(Status, DomainsWithoutDkim, CustomDomains), "RequirementMet" : Status @@ -158,7 +158,7 @@ tests[{ "Requirement" : "A DMARC policy SHALL be published for every second-level domain", "Control" : "EXO 2.4", "Criticality" : "Shall", - "Commandlet" : "Resolve-DnsName", + "Commandlet" : ["Get-ScubaDmarcRecords", "Get-AcceptedDomain"], "ActualValue" : input.dmarc_records, "ReportDetails" : ReportDetailsArray(Status, Domains, AllDomains), "RequirementMet" : Status @@ -180,7 +180,7 @@ tests[{ "Requirement" : "The DMARC message rejection option SHALL be \"p=reject\"", "Control" : "EXO 2.4", "Criticality" : "Shall", - "Commandlet" : "Resolve-DnsName", + "Commandlet" : ["Get-ScubaDmarcRecords", "Get-AcceptedDomain"], "ActualValue" : input.dmarc_records, "ReportDetails" : ReportDetailsArray(Status, Domains, AllDomains), "RequirementMet" : Status @@ -202,7 +202,7 @@ tests[{ "Requirement" : "The DMARC point of contact for aggregate reports SHALL include reports@dmarc.cyber.dhs.gov", "Control" : "EXO 2.4", "Criticality" : "Shall", - "Commandlet" : "Resolve-DnsName", + "Commandlet" : ["Get-ScubaDmarcRecords", "Get-AcceptedDomain"], "ActualValue" : input.dmarc_records, "ReportDetails" : ReportDetailsArray(Status, Domains, AllDomains), "RequirementMet" : Status @@ -223,8 +223,8 @@ DomainsWithoutAgencyContact[DmarcRecord.domain] { tests[{ "Requirement" : "An agency point of contact SHOULD be included for aggregate and/or failure reports", "Control" : "EXO 2.4", - "Criticality" : "Shall", - "Commandlet" : "Resolve-DnsName", + "Criticality" : "Should", + "Commandlet" : ["Get-ScubaDmarcRecords", "Get-AcceptedDomain"], "ActualValue" : input.dmarc_records, "ReportDetails" : ReportDetailsArray(Status, Domains, AllDomains), "RequirementMet" : Status @@ -242,17 +242,22 @@ tests[{ # # Baseline 2.5: Policy 1 #-- + +SmtpClientAuthEnabled[TransportConfig.Name] { + TransportConfig := input.transport_config[_] + TransportConfig.SmtpClientAuthenticationDisabled == false +} + tests[{ "Requirement" : "SMTP AUTH SHALL be disabled in Exchange Online", "Control" : "EXO 2.5", "Criticality" : "Shall", - "Commandlet" : "Get-TransportConfig", - "ActualValue" : TransportConfig.SmtpClientAuthenticationDisabled, + "Commandlet" : ["Get-TransportConfig"], + "ActualValue" : input.transport_config, "ReportDetails" : ReportDetailsBoolean(Status), "RequirementMet" : Status }] { - TransportConfig := input.transport_config - Status := TransportConfig.SmtpClientAuthenticationDisabled == true + Status := count(SmtpClientAuthEnabled) == 0 } #-- @@ -266,38 +271,44 @@ tests[{ # # Baseline 2.6: Policy 1 #-- + +SharingPolicyAllowedSharing[SharingPolicy.Name] { + SharingPolicy := input.sharing_policy[_] + InList := "*" in SharingPolicy.Domains + InList == true +} + + tests[{ "Requirement" : "Contact folders SHALL NOT be shared with all domains, although they MAY be shared with specific domains", "Control" : "EXO 2.6", "Criticality" : "Shall", - "Commandlet" : "Get-SharingPolicy", - "ActualValue" : SharingPolicy.Domains, + "Commandlet" : ["Get-SharingPolicy"], + "ActualValue" : input.sharing_policy, "ReportDetails" : ReportDetailsString(Status, ErrorMessage), "RequirementMet" : Status }] { - SharingPolicy := input.sharing_policy - InList := "*" in SharingPolicy.Domains - ErrorMessage := "Wildcard domain (\"*\") in shared domains list, enabling sharing will all domains by default" - Status := InList == false + ErrorMessage := "Wildcard domain (\"*\") in shared domains list, enabling sharing with all domains by default" + + Status := count(SharingPolicyAllowedSharing) == 0 } #-- # # Baseline 2.6: Policy 2 #-- + tests[{ "Requirement" : "Calendar details SHALL NOT be shared with all domains, although they MAY be shared with specific domains", "Control" : "EXO 2.6", "Criticality" : "Shall", - "Commandlet" : "Get-SharingPolicy", - "ActualValue" : SharingPolicy.Domains, + "Commandlet" : ["Get-SharingPolicy"], + "ActualValue" : input.sharing_policy, "ReportDetails" : ReportDetailsString(Status, ErrorMessage), "RequirementMet" : Status }] { - SharingPolicy := input.sharing_policy - InList := "*" in SharingPolicy.Domains - ErrorMessage := "Wildcard domain (\"*\") in shared domains list, enabling sharing will all domains by default" - Status := InList == false + ErrorMessage := "Wildcard domain (\"*\") in shared domains list, enabling sharing with all domains by default" + Status := count(SharingPolicyAllowedSharing) == 0 } #-- @@ -313,7 +324,7 @@ tests[{ "Requirement" : "External sender warnings SHALL be implemented", "Control" : "EXO 2.7", "Criticality" : "Shall", - "Commandlet" : "Get-TransportRule", + "Commandlet" : ["Get-TransportRule"], "ActualValue" : [Rule.FromScope | Rule = Rules[_]], "ReportDetails" : ReportDetailsString(Status, ErrorMessage), "RequirementMet" : Status @@ -338,7 +349,7 @@ tests[{ "Requirement" : "A DLP solution SHALL be used. The selected DLP solution SHOULD offer services comparable to the native DLP solution offered by Microsoft", "Control" : "EXO 2.8", "Criticality" : "Shall/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false @@ -355,7 +366,7 @@ tests[{ "Requirement" : "The DLP solution SHALL protect PII and sensitive information, as defined by the agency. At a minimum, the sharing of credit card numbers, Taxpayer Identification Numbers (TIN), and Social Security Numbers (SSN) via email SHALL be restricted", "Control" : "EXO 2.8", "Criticality" : "Shall/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false @@ -377,7 +388,7 @@ tests[{ "Requirement" : "Emails SHALL be filtered by the file types of included attachments. The selected filtering solution SHOULD offer services comparable to Microsoft Defender's Common Attachment Filter", "Control" : "EXO 2.9", "Criticality" : "Shall/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false @@ -394,7 +405,7 @@ tests[{ "Requirement" : "The attachment filter SHOULD attempt to determine the true file type and assess the file extension", "Control" : "EXO 2.9", "Criticality" : "Should/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false @@ -411,7 +422,7 @@ tests[{ "Requirement" : "Disallowed file types SHALL be determined and set. At a minimum, click-to-run files SHOULD be blocked (e.g., .exe, .cmd, and .vbe)", "Control" : "EXO 2.9", "Criticality" : "Shall/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false @@ -433,7 +444,7 @@ tests[{ "Requirement" : "Emails SHALL be scanned for malware", "Control" : "EXO 2.10", "Criticality" : "Shall/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false @@ -450,7 +461,7 @@ tests[{ "Requirement" : "Emails identified as containing malware SHALL be quarantined or dropped", "Control" : "EXO 2.10", "Criticality" : "Shall/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false @@ -467,7 +478,7 @@ tests[{ "Requirement" : "Email scanning SHOULD be capable of reviewing emails after delivery", "Control" : "EXO 2.10", "Criticality" : "Should/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false @@ -489,7 +500,7 @@ tests[{ "Requirement" : "Impersonation protection checks SHOULD be used", "Control" : "EXO 2.11", "Criticality" : "Should/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false @@ -506,7 +517,7 @@ tests[{ "Requirement" : "User warnings, comparable to the user safety tips included with EOP, SHOULD be displayed", "Control" : "EXO 2.11", "Criticality" : "Should/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false @@ -523,7 +534,7 @@ tests[{ "Requirement" : "The phishing protection solution SHOULD include an AI-based phishing detection tool comparable to EOP Mailbox Intelligence", "Control" : "EXO 2.11", "Criticality" : "Should/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false @@ -540,35 +551,45 @@ tests[{ # # Baseline 2.12: Policy 1 #-- + +ConnFiltersWithIPAllowList[ConnFilter.Name] { + ConnFilter := input.conn_filter[_] + count(ConnFilter.IPAllowList) > 0 +} + tests[{ "Requirement" : "IP allow lists SHOULD NOT be created", "Control" : "EXO 2.12", "Criticality" : "Should", - "Commandlet" : "Get-HostedConnectionFilterPolicy", - "ActualValue" : AllowList, + "Commandlet" : ["Get-HostedConnectionFilterPolicy"], + "ActualValue" : input.conn_filter, "ReportDetails" : ReportDetailsString(Status, ErrorMessage), "RequirementMet" : Status }]{ - AllowList := input.conn_filter.IPAllowList ErrorMessage := "Allow-list is in use" - Status := count(AllowList) == 0 + Status := count(ConnFiltersWithIPAllowList) == 0 } #-- # # Baseline 2.12: Policy 2 #-- + +ConnFiltersWithSafeList[ConnFilter.Name] { + ConnFilter := input.conn_filter[_] + ConnFilter.EnableSafeList == true +} + tests[{ "Requirement" : "Safe lists SHOULD NOT be enabled", "Control" : "EXO 2.12", "Criticality" : "Should", - "Commandlet" : "Get-HostedConnectionFilterPolicy", - "ActualValue" : EnableSafeList, + "Commandlet" : ["Get-HostedConnectionFilterPolicy"], + "ActualValue" : input.conn_filter, "ReportDetails" : ReportDetailsBoolean(Status), "RequirementMet" : Status }]{ - EnableSafeList := input.conn_filter.EnableSafeList - Status := EnableSafeList == false + Status := count(ConnFiltersWithSafeList) == 0 } #-- @@ -580,17 +601,21 @@ tests[{ # # Baseline 2.13: Policy 1 #-- +AuditEnabled[OrgConfig.Name] { + OrgConfig := input.org_config[_] + OrgConfig.AuditDisabled == true +} + tests[{ "Requirement" : "Mailbox auditing SHALL be enabled", "Control" : "EXO 2.13", "Criticality" : "Shall", - "Commandlet" : "Get-OrganizationConfig", - "ActualValue" : AuditDisabled, + "Commandlet" : ["Get-OrganizationConfig"], + "ActualValue" : input.org_config, "ReportDetails" : ReportDetailsBoolean(Status), "RequirementMet" : Status }] { - AuditDisabled := input.org_config.AuditDisabled - Status := AuditDisabled == false + Status := count(AuditEnabled) == 0 } #-- @@ -607,7 +632,7 @@ tests[{ "Requirement" : "A spam filter SHALL be enabled. The filtering solution selected SHOULD offer services comparable to the native spam filtering offered by Microsoft", "Control" : "EXO 2.14", "Criticality" : "Shall/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false @@ -624,7 +649,7 @@ tests[{ "Requirement" : "Spam and high confidence spam SHALL be moved to either the junk email folder or the quarantine folder", "Control" : "EXO 2.14", "Criticality" : "Shall/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false @@ -641,7 +666,7 @@ tests[{ "Requirement" : "Allowed senders MAY be added, but allowed domains SHALL NOT be added", "Control" : "EXO 2.14", "Criticality" : "Shall/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false @@ -663,7 +688,7 @@ tests[{ "Requirement" : "URL comparison with a block-list SHOULD be enabled", "Control" : "EXO 2.15", "Criticality" : "Should/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false @@ -680,7 +705,7 @@ tests[{ "Requirement" : "Direct download links SHOULD be scanned for malware", "Control" : "EXO 2.15", "Criticality" : "Should/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false @@ -697,7 +722,7 @@ tests[{ "Requirement" : "User click tracking SHOULD be enabled", "Control" : "EXO 2.15", "Criticality" : "Should/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false @@ -719,7 +744,7 @@ tests[{ "Requirement" : "At a minimum, the following alerts SHALL be enabled...[see Exchange Online secure baseline for list]", "Control" : "EXO 2.16", "Criticality" : "Shall/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false @@ -736,7 +761,7 @@ tests[{ "Requirement" : "The alerts SHOULD be sent to a monitored address or incorporated into a SIEM", "Control" : "EXO 2.16", "Criticality" : "Should/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false @@ -758,7 +783,7 @@ tests[{ "Requirement" : "Unified audit logging SHALL be enabled", "Control" : "EXO 2.17", "Criticality" : "Shall/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false @@ -775,7 +800,7 @@ tests[{ "Requirement" : "Advanced audit SHALL be enabled", "Control" : "EXO 2.17", "Criticality" : "Shall/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false @@ -792,7 +817,7 @@ tests[{ "Requirement" : "Audit logs SHALL be maintained for at least the minimum duration dictated by OMB M-21-31", "Control" : "EXO 2.17", "Criticality" : "Shall/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false diff --git a/Rego/OneDriveConfig.rego b/Rego/OneDriveConfig.rego index 26a7e3bb34..2ac983faf1 100644 --- a/Rego/OneDriveConfig.rego +++ b/Rego/OneDriveConfig.rego @@ -13,17 +13,22 @@ ReportDetailsBoolean(Status) = "Requirement not met" if {Status == false} # # Baseline 2.1: Policy 1 #-- +AnyoneLinksPolicy[Policy]{ + Policy := input.SPO_tenant_info[_] + Policy.OneDriveLoopSharingCapability == 1 +} + tests[{ "Requirement" : "Anyone links SHOULD be disabled", "Control" : "OneDrive 2.1", "Criticality" : "Should", - "Commandlet" : "Get-SPOTenant", - "ActualValue" : TenantInfo.OneDriveLoopSharingCapability, + "Commandlet" : ["Get-SPOTenant"], + "ActualValue" : Policies, "ReportDetails" : ReportDetailsBoolean(Status), "RequirementMet" : Status }] { - TenantInfo := input.SPO_tenant_info - Status := TenantInfo.OneDriveLoopSharingCapability == 1 + Policies := AnyoneLinksPolicy + Status := count(Policies) == 1 } #-- @@ -35,34 +40,44 @@ tests[{ # # Baseline 2.2: Policy 1 #-- +RequiredExpirationDatePolicy[Policy]{ + Policy := input.SPO_tenant_info[_] + Policy.ExternalUserExpirationRequired == true +} + tests[{ "Requirement" : "An expiration date SHOULD be set for Anyone links", "Control" : "OneDrive 2.2", "Criticality" : "Should", - "Commandlet" : "Get-SPOTenant", - "ActualValue" : TenantInfo.ExternalUserExpirationRequired, + "Commandlet" : ["Get-SPOTenant"], + "ActualValue" : Policies, "ReportDetails" : ReportDetailsBoolean(Status), "RequirementMet" : Status }] { - TenantInfo := input.SPO_tenant_info - Status := TenantInfo.ExternalUserExpirationRequired== true + Policies := RequiredExpirationDatePolicy + Status := count(Policies) == 1 } #-- # # Baseline 2.2: Policy 2 #-- +ExpirationDatePolicy[Policy]{ + Policy := input.SPO_tenant_info[_] + Policy.ExternalUserExpireInDays == 30 +} + tests[{ "Requirement" : "Expiration date SHOULD be set to thirty days", "Control" : "OneDrive 2.2", "Criticality" : "Should", - "Commandlet" : "Get-SPOTenant", - "ActualValue" : TenantInfo.ExternalUserExpireInDays, + "Commandlet" : ["Get-SPOTenant"], + "ActualValue" : Policies, "ReportDetails" : ReportDetailsBoolean(Status), "RequirementMet" : Status }] { - TenantInfo := input.SPO_tenant_info - Status := TenantInfo.ExternalUserExpireInDays == 30 + Policies := ExpirationDatePolicy + Status := count(Policies) == 1 } #-- @@ -74,17 +89,22 @@ tests[{ # # Baseline 2.3: Policy 1 #-- +DefaultLinkPermissionPolicy[Policy]{ + Policy := input.SPO_tenant_info[_] + Policy.DefaultLinkPermission == 1 +} + tests[{ "Requirement" : "Anyone link permissions SHOULD be limited to View", "Control" : "OneDrive 2.3", "Criticality" : "Should", - "Commandlet" : "Get-SPOTenant", - "ActualValue" : TenantInfo.DefaultLinkPermission, + "Commandlet" : ["Get-SPOTenant"], + "ActualValue" : Policies, "ReportDetails" : ReportDetailsBoolean(Status), "RequirementMet" : Status }] { - TenantInfo := input.SPO_tenant_info - Status := TenantInfo.DefaultLinkPermission == 1 + Policies := DefaultLinkPermissionPolicy + Status := count(Policies) == 1 } #-- @@ -96,18 +116,22 @@ tests[{ # # Baseline 2.4: Policy 1 #-- +DefinedDomainsPolicy[Policy]{ + Policy := input.Tenant_sync_info[_] + count(Policy.AllowedDomainList) > 0 +} + tests[{ "Requirement" : "OneDrive Client for Windows SHALL be restricted to agency-Defined Domain(s)", "Control" : "OneDrive 2.4", "Criticality" : "Shall", - "Commandlet" : "Get-SPOTenant", - "ActualValue" : ["Domain GUID: ", Domain], + "Commandlet" : ["Get-SPOTenant"], + "ActualValue" : Policies, "ReportDetails" : ReportDetailsBoolean(Status), "RequirementMet" : Status }] { - TenantSyncInfo := input.Tenant_sync_info - Domain := input.Expected_results.Owner - Status := Domain in TenantSyncInfo.AllowedDomainList + Policies := DefinedDomainsPolicy + Status := count(Policies) == 1 } #-- @@ -119,17 +143,22 @@ tests[{ # # Baseline 2.5: Policy 1 #-- +ClientSyncPolicy[Policy]{ + Policy := input.Tenant_sync_info[_] + Policy.BlockMacSync == false +} + tests[{ "Requirement" : "OneDrive Client Sync SHALL only be allowed only within the local domain", "Control" : "OneDrive 2.5", "Criticality" : "Shall", - "Commandlet" : "Get-SPOTenant", - "ActualValue" : TenantSyncInfo.BlockMacSync, + "Commandlet" : ["Get-SPOTenantSyncClientRestriction"], + "ActualValue" : Policies, "ReportDetails" : ReportDetailsBoolean(Status), "RequirementMet" : Status }] { - TenantSyncInfo := input.Tenant_sync_info - Status := TenantSyncInfo.BlockMacSync == false + Policies := ClientSyncPolicy + Status := count(Policies) == 1 } #-- @@ -146,7 +175,7 @@ tests[{ "Requirement" : "OneDrive Client Sync SHALL be restricted to the local domain", "Control" : "OneDrive 2.6", "Criticality" : "Shall/Not-Implemented", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Currently cannot be checked automatically. See Onedrive Secure Configuration Baseline policy 2.6 for instructions on manual check", "RequirementMet" : false @@ -168,11 +197,11 @@ tests[{ "Requirement" : "Legacy Authentication SHALL be blocked", "Control" : "OneDrive 2.7", "Criticality" : "Shall/Not-Implemented", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Currently cannot be checked automatically. See Onedrive Secure Configuration Baseline policy 2.7 for instructions on manual check", "RequirementMet" : false }] { true } -#-- \ No newline at end of file +#-- diff --git a/Rego/SharepointConfig.rego b/Rego/SharepointConfig.rego index 8ea1005f84..856d8f68f7 100644 --- a/Rego/SharepointConfig.rego +++ b/Rego/SharepointConfig.rego @@ -13,17 +13,22 @@ ReportDetailsBoolean(Status) = "Requirement not met" if {Status == false} # # Baseline 2.1: Policy 1 #-- +DefaultSharingLinkTypePolicy[Policy]{ + Policy := input.SPO_tenant[_] + Policy.DefaultSharingLinkType == 1 +} + tests[{ "Requirement" : "File and folder links default sharing setting SHALL be set to \"Specific People (Only the People the User Specifies)\"", "Control" : "Sharepoint 2.1", "Criticality" : "Shall", - "Commandlet" : "Get-SPOTenant", - "ActualValue" : SPOTenant.DefaultSharingLinkType, + "Commandlet" : ["Get-SPOTenant"], + "ActualValue" : Policies, "ReportDetails" : ReportDetailsBoolean(Status), "RequirementMet" : Status }] { - SPOTenant := input.SPO_tenant - Status := SPOTenant.DefaultSharingLinkType == 1 + Policies := DefaultSharingLinkTypePolicy + Status := count(Policies) == 1 } #-- @@ -35,17 +40,22 @@ tests[{ # # Baseline 2.2: Policy 1 #-- +ExternalSharingPolicy[Policy]{ + Policy := input.SPO_tenant[_] + Policy.SharingCapability == 1 +} + tests[{ "Requirement" : "External sharing SHOULD be limited to approved domains and security groups per interagency collaboration needs", "Control" : "Sharepoint 2.2", "Criticality" : "Should", - "Commandlet" : "Get-SPOTenant", - "ActualValue" : SPOTenant.SharingCapability, + "Commandlet" : ["Get-SPOTenant"], + "ActualValue" : Policies, "ReportDetails" : ReportDetailsBoolean(Status), "RequirementMet" : Status }] { - SPOTenant := input.SPO_tenant - Status := SPOTenant.SharingCapability == 1 + Policies := ExternalSharingPolicy + Status := count(Policies) == 1 } #-- @@ -62,7 +72,7 @@ tests[{ "Requirement" : "Sharing settings for specific SharePoint sites SHOULD align to their sensitivity level", "Control" : "Sharepoint 2.3", "Criticality" : "Should/Not-Implemented", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Currently cannot be checked automatically. See Sharepoint Secure Configuration Baseline policy 2.3 for instructions on manual check", "RequirementMet" : false @@ -79,34 +89,44 @@ tests[{ # # Baseline 2.4: Policy 1 #-- +ExpirationTimerPolicyRequired[Policy]{ + Policy := input.SPO_tenant[_] + Policy.ExternalUserExpirationRequired == true +} + tests[{ "Requirement" : "Expiration timers for 'guest access to a site or OneDrive' and 'people who use a verification code' SHOULD be set", "Control" : "Sharepoint 2.4", "Criticality" : "Should", - "Commandlet" : "Get-SPOTenant", - "ActualValue" : SPOTenant.ExternalUserExpirationRequired, + "Commandlet" : ["Get-SPOTenant"], + "ActualValue" : Policies, "ReportDetails" : ReportDetailsBoolean(Status), "RequirementMet" : Status }] { - SPOTenant := input.SPO_tenant - Status := SPOTenant.ExternalUserExpirationRequired == true + Policies := ExpirationTimerPolicyRequired + Status := count(Policies) == 1 } #-- # # Baseline 2.4: Policy 2 #-- +ExpirationTimerPolicy[Policy]{ + Policy := input.SPO_tenant[_] + Policy.ExternalUserExpireInDays == 30 +} + tests[{ "Requirement" : "Expiration timers SHOULD be set to 30 days", "Control" : "Sharepoint 2.4", "Criticality" : "Should", - "Commandlet" : "Get-SPOTenant", - "ActualValue" : SPOTenant.ExternalUserExpireInDays, + "Commandlet" : ["Get-SPOTenant"], + "ActualValue" : Policies, "ReportDetails" : ReportDetailsBoolean(Status), "RequirementMet" : Status }] { - SPOTenant := input.SPO_tenant - Status := SPOTenant.ExternalUserExpireInDays == 30 + Policies := ExpirationTimerPolicy + Status := count(Policies) == 1 } #-- @@ -118,16 +138,41 @@ tests[{ # # Baseline 2.5: Policy 1 #-- +# At this time we are unable to test for X because of Y +tests[{ + "Requirement" : "Users SHALL be prevented from running custom scripts on personal sites (OneDrive)", + "Control" : "Sharepoint 2.5", + "Criticality" : "Shall/Not-Implemented", + "Commandlet" : [], + "ActualValue" : [], + "ReportDetails" : "Currently cannot be checked automatically. See Sharepoint Secure Configuration Baseline policy 2.5 for instructions on manual check", + "RequirementMet" : false +}] { + true +} +#-- + +# +# Baseline 2.5: Policy 2 +#-- +CustomScriptPolicy[Policy]{ + Policy := input.SPO_site[_] + # DenyAddAndCustomizePages corresponds to the Custom Script config in the Sharepoint Admin classic settings page (2nd set of bullets in GUI) + # 1 = Allow users to run custom script on self-service created sites + # 2 = Prevent users from running custom script on self-service created sites + Policy.DenyAddAndCustomizePages == 2 +} + tests[{ - "Requirement" : "Users SHALL be prevented from running custom scripts", + "Requirement" : "Users SHALL be prevented from running custom scripts on self-service created sites", "Control" : "Sharepoint 2.5", "Criticality" : "Shall", - "Commandlet" : "Get-SPOSite -Identity", - "ActualValue" : SPOSite.DenyAddAndCustomizePages, + "Commandlet" : ["Get-SPOSite"], + "ActualValue" : Policies, "ReportDetails" : ReportDetailsBoolean(Status), "RequirementMet" : Status }] { - SPOSite := input.SPO_site - Status := SPOSite.DenyAddAndCustomizePages == 1 + Policies := CustomScriptPolicy + Status := count(Policies) == 1 } #-- \ No newline at end of file diff --git a/Rego/TeamsConfig.rego b/Rego/TeamsConfig.rego index e07d027e6d..340702bd14 100644 --- a/Rego/TeamsConfig.rego +++ b/Rego/TeamsConfig.rego @@ -50,7 +50,7 @@ tests[{ "Requirement" : "External participants SHOULD NOT be enabled to request control of shared desktops or windows in the Global (Org-wide default) meeting policy or in custom meeting policies if any exist", "Control" : "Teams 2.1", "Criticality" : "Should", - "Commandlet" : "Get-CsTeamsMeetingPolicy", + "Commandlet" : ["Get-CsTeamsMeetingPolicy"], "ActualValue" : Policies, "ReportDetails" : ReportDetailsArray(Status, Policies, String), "RequirementMet" : Status @@ -78,7 +78,7 @@ tests[{ "Requirement" : "Anonymous users SHALL NOT be enabled to start meetings in the Global (Org-wide default) meeting policy or in custom meeting policies if any exist", "Control" : "Teams 2.2", "Criticality" : "Shall", - "Commandlet" : "Get-CsTeamsMeetingPolicy", + "Commandlet" : ["Get-CsTeamsMeetingPolicy"], "ActualValue" : Policies, "ReportDetails" : ReportDetailsArray(Status, Policies, String), "RequirementMet" : Status @@ -118,7 +118,7 @@ tests[{ "Requirement" : "Anonymous users, including dial-in users, SHOULD NOT be admitted automatically", "Control" : "Teams 2.3", "Criticality" : "Should", - "Commandlet" : "Get-CsTeamsMeetingPolicy", + "Commandlet" : ["Get-CsTeamsMeetingPolicy"], "ActualValue" : [Policy.AutoAdmittedUsers, Policy.AllowPSTNUsersToBypassLobby], "ReportDetails" : ReportDetails2_3(Policy), "RequirementMet" : Status @@ -129,6 +129,18 @@ tests[{ Conditions := [Policy.AutoAdmittedUsers != "Everyone", Policy.AllowPSTNUsersToBypassLobby == false] Status := count([Condition | Condition = Conditions[_]; Condition == false]) == 0 } + +tests[{ + "Requirement" : "Anonymous users, including dial-in users, SHOULD NOT be admitted automatically", + "Control" : "Teams 2.3", + "Criticality" : "Should", + "Commandlet" : ["Get-CsTeamsMeetingPolicy"], + "ActualValue" : "PowerShell Error", + "ReportDetails" : "PowerShell Error", + "RequirementMet" : false +}] { + count(input.meeting_policies) == 0 +} #-- # @@ -138,7 +150,7 @@ tests[{ "Requirement" : "Internal users SHOULD be admitted automatically", "Control" : "Teams 2.3", "Criticality" : "Should", - "Commandlet" : "Get-CsTeamsMeetingPolicy", + "Commandlet" : ["Get-CsTeamsMeetingPolicy"], "ActualValue" : Policy.AutoAdmittedUsers, "ReportDetails" : ReportDetailsBoolean(Status), "RequirementMet" : Status @@ -148,6 +160,21 @@ tests[{ Policy.Identity = "Global" Status := Policy.AutoAdmittedUsers in ["EveryoneInCompany", "EveryoneInSameAndFederatedCompany", "EveryoneInCompanyExcludingGuests"] } + +# +# Baseline 2.3: Policy 2 +#-- +tests[{ + "Requirement" : "Internal users SHOULD be admitted automatically", + "Control" : "Teams 2.3", + "Criticality" : "Should", + "Commandlet" : ["Get-CsTeamsMeetingPolicy"], + "ActualValue" : "PowerShell Error", + "ReportDetails" : "PowerShell Error", + "RequirementMet" : false +}] { + count(input.meeting_policies) == 0 +} #-- @@ -169,7 +196,7 @@ tests[{ "Requirement" : "External access SHALL only be enabled on a per-domain basis", "Control" : "Teams 2.4", "Criticality" : "Shall", - "Commandlet" : "Get-CsTenantFederationConfiguration", + "Commandlet" : ["Get-CsTenantFederationConfiguration"], "ActualValue" : Policies, "ReportDetails" : ReportDetailsArray(Status, Policies, String), "RequirementMet" : Status @@ -192,7 +219,7 @@ tests[{ "Requirement" : "Anonymous users SHOULD be enabled to join meetings", "Control" : "Teams 2.4", "Criticality" : "Should", - "Commandlet" : "Get-CsTeamsMeetingPolicy", + "Commandlet" : ["Get-CsTeamsMeetingPolicy"], "ActualValue" : MeetingsNotAllowingAnonJoin, "ReportDetails" : ReportDetailsArray(Status, Policies, String), "RequirementMet" : Status @@ -238,7 +265,7 @@ tests[{ "Requirement" : "Unmanaged users SHALL NOT be enabled to initiate contact with internal users", "Control" : "Teams 2.5", "Criticality" : "Shall", - "Commandlet" : "Get-CsTenantFederationConfiguration", + "Commandlet" : ["Get-CsTenantFederationConfiguration"], "ActualValue" : Policies, "ReportDetails" : ReportDetailsArray(Status, Policies, String), "RequirementMet" : Status @@ -261,7 +288,7 @@ tests[{ "Requirement" : "Internal users SHOULD NOT be enabled to initiate contact with unmanaged users", "Control" : "Teams 2.5", "Criticality" : "Should", - "Commandlet" : "Get-CsTenantFederationConfiguration", + "Commandlet" : ["Get-CsTenantFederationConfiguration"], "ActualValue" : Policies, "ReportDetails" : ReportDetailsArray(Status, Policies, String), "RequirementMet" : Status @@ -289,7 +316,7 @@ tests[{ "Requirement" : "Contact with Skype users SHALL be blocked", "Control" : "Teams 2.6", "Criticality" : "Shall", - "Commandlet" : "Get-CsTenantFederationConfiguration", + "Commandlet" : ["Get-CsTenantFederationConfiguration"], "ActualValue" : Policies, "ReportDetails" : ReportDetailsArray(Status, Policies, String), "RequirementMet" : Status @@ -335,7 +362,7 @@ tests[{ "Requirement" : "Teams email integration SHALL be disabled", "Control" : "Teams 2.7", "Criticality" : "Shall", - "Commandlet" : "Get-CsTeamsClientConfiguration", + "Commandlet" : ["Get-CsTeamsClientConfiguration"], "ActualValue" : [Policies, ServiceInstance], "ReportDetails" : ReportDetails2_7(IsGCC, ComfirmCorrectConfig, Policies), "RequirementMet" : Status @@ -348,6 +375,18 @@ tests[{ Conditions := [ComfirmCorrectConfig, IsGCC] Status := count([Condition | Condition = Conditions[_]; Condition == true]) > 0 } + +tests[{ + "Requirement" : "Teams email integration SHALL be disabled", + "Control" : "Teams 2.7", + "Criticality" : "Shall", + "Commandlet" : ["Get-CsTeamsClientConfiguration"], + "ActualValue" : "PowerShell Error", + "ReportDetails" : "PowerShell Error", + "RequirementMet" : false +}] { + count(input.teams_tenant_info) == 0 +} #-- @@ -367,7 +406,7 @@ tests[{ "Requirement" : "Agencies SHOULD allow all apps published by Microsoft, but MAY block specific Microsoft apps as needed", "Control" : "Teams 2.8", "Criticality" : "Should", - "Commandlet" : "Get-CsTeamsAppPermissionPolicy", + "Commandlet" : ["Get-CsTeamsAppPermissionPolicy"], "ActualValue" : Policies, "ReportDetails" : ReportDetailsArray(Status, Policies, String), "RequirementMet" : Status @@ -395,7 +434,7 @@ tests[{ "Requirement" : "Agencies SHOULD NOT allow installation of all third-party apps, but MAY allow specific apps as needed", "Control" : "Teams 2.8", "Criticality" : "Should", - "Commandlet" : "Get-CsTeamsAppPermissionPolicy", + "Commandlet" : ["Get-CsTeamsAppPermissionPolicy"], "ActualValue" : Policies, "ReportDetails" : ReportDetailsArray(Status, Policies, String), "RequirementMet" : Status @@ -409,7 +448,7 @@ tests[{ "Requirement" : "Agencies SHOULD NOT allow installation of all custom apps, but MAY allow specific apps as needed", "Control" : "Teams 2.8", "Criticality" : "Should", - "Commandlet" : "Get-CsTeamsAppPermissionPolicy", + "Commandlet" : ["Get-CsTeamsAppPermissionPolicy"], "ActualValue" : Policies, "ReportDetails" : ReportDetailsArray(Status, Policies, String), "RequirementMet" : Status @@ -428,7 +467,7 @@ tests[{ "Requirement" : "Agencies SHALL establish policy dictating the app review and approval process to be used by the agency", "Control" : "Teams 2.8", "Criticality" : "Shall/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Cannot be checked automatically. See Microsoft Teams Secure Configuration Baseline policy 2.8 for instructions on manual check", "RequirementMet" : false @@ -449,16 +488,30 @@ tests[{ "Requirement" : "Cloud video recording SHOULD be disabled in the global (org-wide default) meeting policy", "Control" : "Teams 2.9", "Criticality" : "Should", - "Commandlet" : "Get-CsTeamsMeetingPolicy", + "Commandlet" : ["Get-CsTeamsMeetingPolicy"], "ActualValue" : Policy.AllowCloudRecording, "ReportDetails" : ReportDetailsBoolean(Status), - "ExpectedValue" : "false", "RequirementMet" : Status }] { Policy := input.meeting_policies[_] Policy.Identity == "Global" # Filter: this control only applies to the Global policy Status := Policy.AllowCloudRecording == false } + +# +# Baseline 2.9: Policy 1 +#-- +tests[{ + "Requirement" : "Cloud video recording SHOULD be disabled in the global (org-wide default) meeting policy", + "Control" : "Teams 2.9", + "Criticality" : "Should", + "Commandlet" : ["Get-CsTeamsMeetingPolicy"], + "ActualValue" : "PowerShell Error", + "ReportDetails" : "PowerShell Error", + "RequirementMet" : false +}] { + count(input.meeting_policies) == 0 +} #-- # @@ -471,10 +524,10 @@ PoliciesAllowingOutsideRegionStorage[Policy.Identity] { } tests[{ - "Requirement" : "For all meeting polices that allow cloud recording, recordings SHOULD be stored inside the country of that agency’s tenant", + "Requirement" : "For all meeting polices that allow cloud recording, recordings SHOULD be stored inside the country of that agency's tenant", "Control" : "Teams 2.9", "Criticality" : "Should", - "Commandlet" : "Get-CsTeamsMeetingPolicy", + "Commandlet" : ["Get-CsTeamsMeetingPolicy"], "ActualValue" : Policies, "ReportDetails" : ReportDetailsArray(Status, Policies, String), "RequirementMet" : Status @@ -497,7 +550,7 @@ tests[{ "Requirement" : "Record an event SHOULD be set to Organizer can record", "Control" : "Teams 2.10", "Criticality" : "Should", - "Commandlet" : "Get-CsTeamsMeetingPolicy", + "Commandlet" : ["Get-CsTeamsMeetingPolicy"], "ActualValue" : Policy.BroadcastRecordingMode, "ReportDetails" : ReportDetailsBoolean(Status), "RequirementMet" : Status @@ -506,6 +559,21 @@ tests[{ Policy.Identity == "Global" # Filter: this control only applies to the Global policy Status := Policy.BroadcastRecordingMode == "UserOverride" } + +# +# Baseline 2.10: Policy 1 +#-- +tests[{ + "Requirement" : "Record an event SHOULD be set to Organizer can record", + "Control" : "Teams 2.10", + "Criticality" : "Should", + "Commandlet" : ["Get-CsTeamsMeetingPolicy"], + "ActualValue" : "PowerShell Error", + "ReportDetails" : "PowerShell Error", + "RequirementMet" : false +}] { + count(input.broadcast_policies) == 0 +} #-- @@ -521,7 +589,7 @@ tests[{ "Requirement" : "A DLP solution SHALL be enabled", "Control" : "Teams 2.11", "Criticality" : "Shall/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false @@ -538,7 +606,7 @@ tests[{ "Requirement" : "Agencies SHOULD use either the native DLP solution offered by Microsoft or a DLP solution that offers comparable services", "Control" : "Teams 2.11", "Criticality" : "Should/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false @@ -555,7 +623,7 @@ tests[{ "Requirement" : "The DLP solution SHALL protect Personally Identifiable Information (PII) and sensitive information, as defined by the agency. At a minimum, the sharing of credit card numbers, taxpayer Identification Numbers (TIN), and Social Security Numbers (SSN) via email SHALL be restricted", "Control" : "Teams 2.11", "Criticality" : "Shall/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false @@ -577,7 +645,7 @@ tests[{ "Requirement" : "Attachments included with Teams messages SHOULD be scanned for malware", "Control" : "Teams 2.12", "Criticality" : "Should/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false @@ -594,7 +662,7 @@ tests[{ "Requirement" : "Users SHOULD be prevented from opening or downloading files detected as malware", "Control" : "Teams 2.12", "Criticality" : "Should/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false @@ -616,7 +684,7 @@ tests[{ "Requirement" : "URL comparison with a block-list SHOULD be enabled", "Control" : "Teams 2.13", "Criticality" : "Should/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false @@ -633,7 +701,7 @@ tests[{ "Requirement" : "Direct download links SHOULD be scanned for malware", "Control" : "Teams 2.13", "Criticality" : "Should/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false @@ -650,7 +718,7 @@ tests[{ "Requirement" : "User click tracking SHOULD be enabled", "Control" : "Teams 2.13", "Criticality" : "Should/3rd Party", - "Commandlet" : "", + "Commandlet" : [], "ActualValue" : [], "ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check", "RequirementMet" : false diff --git a/SetUp.ps1 b/SetUp.ps1 index 630002958f..3e961c8641 100644 --- a/SetUp.ps1 +++ b/SetUp.ps1 @@ -1,29 +1,110 @@ -# -# This script installs the required Powershell modules used by the assessment tool. -# - -$ModuleList = @( - "PowerShellGet", - "MicrosoftTeams", - "ExchangeOnlineManagement", # includes Defender - "Microsoft.Online.SharePoint.PowerShell", # includes OneDrive - "Microsoft.PowerApps.Administration.PowerShell", - "Microsoft.PowerApps.PowerShell", - "Microsoft.Graph.Applications", # starting here, modules for AAD - "Microsoft.Graph.Authentication", - "Microsoft.Graph.DeviceManagement", - "Microsoft.Graph.DeviceManagement.Administration", - "Microsoft.Graph.DeviceManagement.Enrolment", - "Microsoft.Graph.Devices.CorporateManagement", - "Microsoft.Graph.Groups", - "Microsoft.Graph.Identity.DirectoryManagement", - "Microsoft.Graph.Identity.Governance", - "Microsoft.Graph.Identity.SignIns", - "Microsoft.Graph.Planner", - "Microsoft.Graph.Teams", - "Microsoft.Graph.Users" - ) - -foreach($Module in $ModuleList) { - Install-Module -Name $Module -Force -AllowClobber -Scope CurrentUser -Verbose +#Requires -Version 5.1 +<# + .SYNOPSIS + This script installs the required Powershell modules used by the + assessment tool + .DESCRIPTION + Installs the modules required to support SCuBAGear. If the Force + switch is set then any existing module will be re-installed even if + already at latest version. If the SkipUpdate switch is set then any + existing module will not be updated to th latest version. + .EXAMPLE + .\Setup.ps1 + .NOTES + Executing the script with no switches set will install the latest + version of a module if not already installed. +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $false, HelpMessage = 'Installs a given module and overrides warning messages about module installation conflicts. If a module with the same name already exists on the computer, Force allows for multiple versions to be installed. If there is an existing module with the same name and version, Force overwrites that version')] + [switch] + $Force, + + [Parameter(HelpMessage = 'If specified then modules will not be updated to latest version')] + [switch] + $SkipUpdate, + + [Parameter(HelpMessage = 'Do not automatically trust the PSGallery repository for module installation')] + [switch] + $DoNotAutoTrustRepository +) + +# Set prefernces for writing messages +$DebugPreference = "Continue" +$InformationPreference = "Continue" + +if (-not $DoNotAutoTrustRepository){ + $Policy = Get-PSRepository -Name "PSGallery" | Select-Object -Property -InstallationPolicy + + if ($($Policy.InstallationPolicy) -ne "Trusted"){ + Set-PSRepository -Name "PSGallery" -InstallationPolicy Trusted + Write-Information -MessageData "Setting PSGallery repository to trusted." + } +} + +# Start a stopwatch to time module installation elapsed time +$Stopwatch = [System.Diagnostics.Stopwatch]::StartNew() + +$RequiredModulesPath = Join-Path -Path $PSScriptRoot -ChildPath "PowerShell\ScubaGear\RequiredVersions.ps1" +if (Test-Path -Path $RequiredModulesPath){ + . $RequiredModulesPath +} + +if ($ModuleList){ + # Add PowerShellGet to beginning of ModuleList for installing required modules. + $ModuleList = ,@{ + ModuleName = 'PowerShellGet' + ModuleVersion = [version] '2.1.0' + MaximumVersion = [version] '2.99.99999' + } + $ModuleList } +else +{ + throw "Required modules list is required." +} + +foreach ($Module in $ModuleList) { + + $ModuleName = $Module.ModuleName + + if (Get-Module -ListAvailable -Name $ModuleName) { + $HighestInstalledVersion = (Get-Module -ListAvailable -Name $ModuleName | Sort-Object Version -Descending | Select-Object Version -First 1).Version + $LatestVersion = (Find-Module -Name $ModuleName).Version + + if ($HighestInstalledVersion -ge $LatestVersion) { + Write-Debug "${ModuleName}:${HighestInstalledVersion} already has latest installed." + + if ($Force -eq $true) { + Install-Module -Name $ModuleName ` + -Force ` + -AllowClobber ` + -Scope CurrentUser + Write-Information -MessageData "Re-installing module: ${ModuleName}" + } + } else { + + if ($SkipUpdate -eq $true) { + Write-Debug "Skipping update for ${ModuleName}:${HighestInstalledVersion} to newer version ${LatestVersion}." + } + else { + Install-Module -Name $ModuleName ` + -Force ` + -AllowClobber ` + -Scope CurrentUser + Write-Information -MessageData " ${ModuleName}:${HighestInstalledVersion} updated to version ${LatestVersion}." + } + } + } else { + Install-Module -Name $ModuleName ` + -AllowClobber ` + -Scope CurrentUser + Write-Information -MessageData "Installed latest version of $ModuleName" + } +} + +# Stop the clock and report total elapsed time +$Stopwatch.stop() + +Write-Debug "ScubaGear setup time elapsed: $([math]::Round($stopwatch.Elapsed.TotalSeconds,0)) seconds." + diff --git a/Testing/Functional/Auto/ExtremeTest.txt b/Testing/Functional/Auto/ExtremeTest.txt new file mode 100644 index 0000000000..33d71b0b92 --- /dev/null +++ b/Testing/Functional/Auto/ExtremeTest.txt @@ -0,0 +1,1956 @@ +aad +defender +exo +onedrive +sharepoint +teams +aad, defender +aad, exo +aad, onedrive +aad, sharepoint +aad, teams +defender, aad +defender, exo +defender, onedrive +defender, sharepoint +defender, teams +exo, aad +exo, defender +exo, onedrive +exo, sharepoint +exo, teams +onedrive, aad +onedrive, defender +onedrive, exo +onedrive, sharepoint +onedrive, teams +sharepoint, aad +sharepoint, defender +sharepoint, exo +sharepoint, onedrive +sharepoint, teams +teams, aad +teams, defender +teams, exo +teams, onedrive +teams, sharepoint +aad, defender, exo +aad, defender, onedrive +aad, defender, sharepoint +aad, defender, teams +aad, exo, defender +aad, exo, onedrive +aad, exo, sharepoint +aad, exo, teams +aad, onedrive, defender +aad, onedrive, exo +aad, onedrive, sharepoint +aad, onedrive, teams +aad, sharepoint, defender +aad, sharepoint, exo +aad, sharepoint, onedrive +aad, sharepoint, teams +aad, teams, defender +aad, teams, exo +aad, teams, onedrive +aad, teams, sharepoint +defender, aad, exo +defender, aad, onedrive +defender, aad, sharepoint +defender, aad, teams +defender, exo, aad +defender, exo, onedrive +defender, exo, sharepoint +defender, exo, teams +defender, onedrive, aad +defender, onedrive, exo +defender, onedrive, sharepoint +defender, onedrive, teams +defender, sharepoint, aad +defender, sharepoint, exo +defender, sharepoint, onedrive +defender, sharepoint, teams +defender, teams, aad +defender, teams, exo +defender, teams, onedrive +defender, teams, sharepoint +exo, aad, defender +exo, aad, onedrive +exo, aad, sharepoint +exo, aad, teams +exo, defender, aad +exo, defender, onedrive +exo, defender, sharepoint +exo, defender, teams +exo, onedrive, aad +exo, onedrive, defender +exo, onedrive, sharepoint +exo, onedrive, teams +exo, sharepoint, aad +exo, sharepoint, defender +exo, sharepoint, onedrive +exo, sharepoint, teams +exo, teams, aad +exo, teams, defender +exo, teams, onedrive +exo, teams, sharepoint +onedrive, aad, defender +onedrive, aad, exo +onedrive, aad, sharepoint +onedrive, aad, teams +onedrive, defender, aad +onedrive, defender, exo +onedrive, defender, sharepoint +onedrive, defender, teams +onedrive, exo, aad +onedrive, exo, defender +onedrive, exo, sharepoint +onedrive, exo, teams +onedrive, sharepoint, aad +onedrive, sharepoint, defender +onedrive, sharepoint, exo +onedrive, sharepoint, teams +onedrive, teams, aad +onedrive, teams, defender +onedrive, teams, exo +onedrive, teams, sharepoint +sharepoint, aad, defender +sharepoint, aad, exo +sharepoint, aad, onedrive +sharepoint, aad, teams +sharepoint, defender, aad +sharepoint, defender, exo +sharepoint, defender, onedrive +sharepoint, defender, teams +sharepoint, exo, aad +sharepoint, exo, defender +sharepoint, exo, onedrive +sharepoint, exo, teams +sharepoint, onedrive, aad +sharepoint, onedrive, defender +sharepoint, onedrive, exo +sharepoint, onedrive, teams +sharepoint, teams, aad +sharepoint, teams, defender +sharepoint, teams, exo +sharepoint, teams, onedrive +teams, aad, defender +teams, aad, exo +teams, aad, onedrive +teams, aad, sharepoint +teams, defender, aad +teams, defender, exo +teams, defender, onedrive +teams, defender, sharepoint +teams, exo, aad +teams, exo, defender +teams, exo, onedrive +teams, exo, sharepoint +teams, onedrive, aad +teams, onedrive, defender +teams, onedrive, exo +teams, onedrive, sharepoint +teams, sharepoint, aad +teams, sharepoint, defender +teams, sharepoint, exo +teams, sharepoint, onedrive +aad, defender, exo, onedrive +aad, defender, exo, sharepoint +aad, defender, exo, teams +aad, defender, onedrive, exo +aad, defender, onedrive, sharepoint +aad, defender, onedrive, teams +aad, defender, sharepoint, exo +aad, defender, sharepoint, onedrive +aad, defender, sharepoint, teams +aad, defender, teams, exo +aad, defender, teams, onedrive +aad, defender, teams, sharepoint +aad, exo, defender, onedrive +aad, exo, defender, sharepoint +aad, exo, defender, teams +aad, exo, onedrive, defender +aad, exo, onedrive, sharepoint +aad, exo, onedrive, teams +aad, exo, sharepoint, defender +aad, exo, sharepoint, onedrive +aad, exo, sharepoint, teams +aad, exo, teams, defender +aad, exo, teams, onedrive +aad, exo, teams, sharepoint +aad, onedrive, defender, exo +aad, onedrive, defender, sharepoint +aad, onedrive, defender, teams +aad, onedrive, exo, defender +aad, onedrive, exo, sharepoint +aad, onedrive, exo, teams +aad, onedrive, sharepoint, defender +aad, onedrive, sharepoint, exo +aad, onedrive, sharepoint, teams +aad, onedrive, teams, defender +aad, onedrive, teams, exo +aad, onedrive, teams, sharepoint +aad, sharepoint, defender, exo +aad, sharepoint, defender, onedrive +aad, sharepoint, defender, teams +aad, sharepoint, exo, defender +aad, sharepoint, exo, onedrive +aad, sharepoint, exo, teams +aad, sharepoint, onedrive, defender +aad, sharepoint, onedrive, exo +aad, sharepoint, onedrive, teams +aad, sharepoint, teams, defender +aad, sharepoint, teams, exo +aad, sharepoint, teams, onedrive +aad, teams, defender, exo +aad, teams, defender, onedrive +aad, teams, defender, sharepoint +aad, teams, exo, defender +aad, teams, exo, onedrive +aad, teams, exo, sharepoint +aad, teams, onedrive, defender +aad, teams, onedrive, exo +aad, teams, onedrive, sharepoint +aad, teams, sharepoint, defender +aad, teams, sharepoint, exo +aad, teams, sharepoint, onedrive +defender, aad, exo, onedrive +defender, aad, exo, sharepoint +defender, aad, exo, teams +defender, aad, onedrive, exo +defender, aad, onedrive, sharepoint +defender, aad, onedrive, teams +defender, aad, sharepoint, exo +defender, aad, sharepoint, onedrive +defender, aad, sharepoint, teams +defender, aad, teams, exo +defender, aad, teams, onedrive +defender, aad, teams, sharepoint +defender, exo, aad, onedrive +defender, exo, aad, sharepoint +defender, exo, aad, teams +defender, exo, onedrive, aad +defender, exo, onedrive, sharepoint +defender, exo, onedrive, teams +defender, exo, sharepoint, aad +defender, exo, sharepoint, onedrive +defender, exo, sharepoint, teams +defender, exo, teams, aad +defender, exo, teams, onedrive +defender, exo, teams, sharepoint +defender, onedrive, aad, exo +defender, onedrive, aad, sharepoint +defender, onedrive, aad, teams +defender, onedrive, exo, aad +defender, onedrive, exo, sharepoint +defender, onedrive, exo, teams +defender, onedrive, sharepoint, aad +defender, onedrive, sharepoint, exo +defender, onedrive, sharepoint, teams +defender, onedrive, teams, aad +defender, onedrive, teams, exo +defender, onedrive, teams, sharepoint +defender, sharepoint, aad, exo +defender, sharepoint, aad, onedrive +defender, sharepoint, aad, teams +defender, sharepoint, exo, aad +defender, sharepoint, exo, onedrive +defender, sharepoint, exo, teams +defender, sharepoint, onedrive, aad +defender, sharepoint, onedrive, exo +defender, sharepoint, onedrive, teams +defender, sharepoint, teams, aad +defender, sharepoint, teams, exo +defender, sharepoint, teams, onedrive +defender, teams, aad, exo +defender, teams, aad, onedrive +defender, teams, aad, sharepoint +defender, teams, exo, aad +defender, teams, exo, onedrive +defender, teams, exo, sharepoint +defender, teams, onedrive, aad +defender, teams, onedrive, exo +defender, teams, onedrive, sharepoint +defender, teams, sharepoint, aad +defender, teams, sharepoint, exo +defender, teams, sharepoint, onedrive +exo, aad, defender, onedrive +exo, aad, defender, sharepoint +exo, aad, defender, teams +exo, aad, onedrive, defender +exo, aad, onedrive, sharepoint +exo, aad, onedrive, teams +exo, aad, sharepoint, defender +exo, aad, sharepoint, onedrive +exo, aad, sharepoint, teams +exo, aad, teams, defender +exo, aad, teams, onedrive +exo, aad, teams, sharepoint +exo, defender, aad, onedrive +exo, defender, aad, sharepoint +exo, defender, aad, teams +exo, defender, onedrive, aad +exo, defender, onedrive, sharepoint +exo, defender, onedrive, teams +exo, defender, sharepoint, aad +exo, defender, sharepoint, onedrive +exo, defender, sharepoint, teams +exo, defender, teams, aad +exo, defender, teams, onedrive +exo, defender, teams, sharepoint +exo, onedrive, aad, defender +exo, onedrive, aad, sharepoint +exo, onedrive, aad, teams +exo, onedrive, defender, aad +exo, onedrive, defender, sharepoint +exo, onedrive, defender, teams +exo, onedrive, sharepoint, aad +exo, onedrive, sharepoint, defender +exo, onedrive, sharepoint, teams +exo, onedrive, teams, aad +exo, onedrive, teams, defender +exo, onedrive, teams, sharepoint +exo, sharepoint, aad, defender +exo, sharepoint, aad, onedrive +exo, sharepoint, aad, teams +exo, sharepoint, defender, aad +exo, sharepoint, defender, onedrive +exo, sharepoint, defender, teams +exo, sharepoint, onedrive, aad +exo, sharepoint, onedrive, defender +exo, sharepoint, onedrive, teams +exo, sharepoint, teams, aad +exo, sharepoint, teams, defender +exo, sharepoint, teams, onedrive +exo, teams, aad, defender +exo, teams, aad, onedrive +exo, teams, aad, sharepoint +exo, teams, defender, aad +exo, teams, defender, onedrive +exo, teams, defender, sharepoint +exo, teams, onedrive, aad +exo, teams, onedrive, defender +exo, teams, onedrive, sharepoint +exo, teams, sharepoint, aad +exo, teams, sharepoint, defender +exo, teams, sharepoint, onedrive +onedrive, aad, defender, exo +onedrive, aad, defender, sharepoint +onedrive, aad, defender, teams +onedrive, aad, exo, defender +onedrive, aad, exo, sharepoint +onedrive, aad, exo, teams +onedrive, aad, sharepoint, defender +onedrive, aad, sharepoint, exo +onedrive, aad, sharepoint, teams +onedrive, aad, teams, defender +onedrive, aad, teams, exo +onedrive, aad, teams, sharepoint +onedrive, defender, aad, exo +onedrive, defender, aad, sharepoint +onedrive, defender, aad, teams +onedrive, defender, exo, aad +onedrive, defender, exo, sharepoint +onedrive, defender, exo, teams +onedrive, defender, sharepoint, aad +onedrive, defender, sharepoint, exo +onedrive, defender, sharepoint, teams +onedrive, defender, teams, aad +onedrive, defender, teams, exo +onedrive, defender, teams, sharepoint +onedrive, exo, aad, defender +onedrive, exo, aad, sharepoint +onedrive, exo, aad, teams +onedrive, exo, defender, aad +onedrive, exo, defender, sharepoint +onedrive, exo, defender, teams +onedrive, exo, sharepoint, aad +onedrive, exo, sharepoint, defender +onedrive, exo, sharepoint, teams +onedrive, exo, teams, aad +onedrive, exo, teams, defender +onedrive, exo, teams, sharepoint +onedrive, sharepoint, aad, defender +onedrive, sharepoint, aad, exo +onedrive, sharepoint, aad, teams +onedrive, sharepoint, defender, aad +onedrive, sharepoint, defender, exo +onedrive, sharepoint, defender, teams +onedrive, sharepoint, exo, aad +onedrive, sharepoint, exo, defender +onedrive, sharepoint, exo, teams +onedrive, sharepoint, teams, aad +onedrive, sharepoint, teams, defender +onedrive, sharepoint, teams, exo +onedrive, teams, aad, defender +onedrive, teams, aad, exo +onedrive, teams, aad, sharepoint +onedrive, teams, defender, aad +onedrive, teams, defender, exo +onedrive, teams, defender, sharepoint +onedrive, teams, exo, aad +onedrive, teams, exo, defender +onedrive, teams, exo, sharepoint +onedrive, teams, sharepoint, aad +onedrive, teams, sharepoint, defender +onedrive, teams, sharepoint, exo +sharepoint, aad, defender, exo +sharepoint, aad, defender, onedrive +sharepoint, aad, defender, teams +sharepoint, aad, exo, defender +sharepoint, aad, exo, onedrive +sharepoint, aad, exo, teams +sharepoint, aad, onedrive, defender +sharepoint, aad, onedrive, exo +sharepoint, aad, onedrive, teams +sharepoint, aad, teams, defender +sharepoint, aad, teams, exo +sharepoint, aad, teams, onedrive +sharepoint, defender, aad, exo +sharepoint, defender, aad, onedrive +sharepoint, defender, aad, teams +sharepoint, defender, exo, aad +sharepoint, defender, exo, onedrive +sharepoint, defender, exo, teams +sharepoint, defender, onedrive, aad +sharepoint, defender, onedrive, exo +sharepoint, defender, onedrive, teams +sharepoint, defender, teams, aad +sharepoint, defender, teams, exo +sharepoint, defender, teams, onedrive +sharepoint, exo, aad, defender +sharepoint, exo, aad, onedrive +sharepoint, exo, aad, teams +sharepoint, exo, defender, aad +sharepoint, exo, defender, onedrive +sharepoint, exo, defender, teams +sharepoint, exo, onedrive, aad +sharepoint, exo, onedrive, defender +sharepoint, exo, onedrive, teams +sharepoint, exo, teams, aad +sharepoint, exo, teams, defender +sharepoint, exo, teams, onedrive +sharepoint, onedrive, aad, defender +sharepoint, onedrive, aad, exo +sharepoint, onedrive, aad, teams +sharepoint, onedrive, defender, aad +sharepoint, onedrive, defender, exo +sharepoint, onedrive, defender, teams +sharepoint, onedrive, exo, aad +sharepoint, onedrive, exo, defender +sharepoint, onedrive, exo, teams +sharepoint, onedrive, teams, aad +sharepoint, onedrive, teams, defender +sharepoint, onedrive, teams, exo +sharepoint, teams, aad, defender +sharepoint, teams, aad, exo +sharepoint, teams, aad, onedrive +sharepoint, teams, defender, aad +sharepoint, teams, defender, exo +sharepoint, teams, defender, onedrive +sharepoint, teams, exo, aad +sharepoint, teams, exo, defender +sharepoint, teams, exo, onedrive +sharepoint, teams, onedrive, aad +sharepoint, teams, onedrive, defender +sharepoint, teams, onedrive, exo +teams, aad, defender, exo +teams, aad, defender, onedrive +teams, aad, defender, sharepoint +teams, aad, exo, defender +teams, aad, exo, onedrive +teams, aad, exo, sharepoint +teams, aad, onedrive, defender +teams, aad, onedrive, exo +teams, aad, onedrive, sharepoint +teams, aad, sharepoint, defender +teams, aad, sharepoint, exo +teams, aad, sharepoint, onedrive +teams, defender, aad, exo +teams, defender, aad, onedrive +teams, defender, aad, sharepoint +teams, defender, exo, aad +teams, defender, exo, onedrive +teams, defender, exo, sharepoint +teams, defender, onedrive, aad +teams, defender, onedrive, exo +teams, defender, onedrive, sharepoint +teams, defender, sharepoint, aad +teams, defender, sharepoint, exo +teams, defender, sharepoint, onedrive +teams, exo, aad, defender +teams, exo, aad, onedrive +teams, exo, aad, sharepoint +teams, exo, defender, aad +teams, exo, defender, onedrive +teams, exo, defender, sharepoint +teams, exo, onedrive, aad +teams, exo, onedrive, defender +teams, exo, onedrive, sharepoint +teams, exo, sharepoint, aad +teams, exo, sharepoint, defender +teams, exo, sharepoint, onedrive +teams, onedrive, aad, defender +teams, onedrive, aad, exo +teams, onedrive, aad, sharepoint +teams, onedrive, defender, aad +teams, onedrive, defender, exo +teams, onedrive, defender, sharepoint +teams, onedrive, exo, aad +teams, onedrive, exo, defender +teams, onedrive, exo, sharepoint +teams, onedrive, sharepoint, aad +teams, onedrive, sharepoint, defender +teams, onedrive, sharepoint, exo +teams, sharepoint, aad, defender +teams, sharepoint, aad, exo +teams, sharepoint, aad, onedrive +teams, sharepoint, defender, aad +teams, sharepoint, defender, exo +teams, sharepoint, defender, onedrive +teams, sharepoint, exo, aad +teams, sharepoint, exo, defender +teams, sharepoint, exo, onedrive +teams, sharepoint, onedrive, aad +teams, sharepoint, onedrive, defender +teams, sharepoint, onedrive, exo +aad, defender, exo, onedrive, sharepoint +aad, defender, exo, onedrive, teams +aad, defender, exo, sharepoint, onedrive +aad, defender, exo, sharepoint, teams +aad, defender, exo, teams, onedrive +aad, defender, exo, teams, sharepoint +aad, defender, onedrive, exo, sharepoint +aad, defender, onedrive, exo, teams +aad, defender, onedrive, sharepoint, exo +aad, defender, onedrive, sharepoint, teams +aad, defender, onedrive, teams, exo +aad, defender, onedrive, teams, sharepoint +aad, defender, sharepoint, exo, onedrive +aad, defender, sharepoint, exo, teams +aad, defender, sharepoint, onedrive, exo +aad, defender, sharepoint, onedrive, teams +aad, defender, sharepoint, teams, exo +aad, defender, sharepoint, teams, onedrive +aad, defender, teams, exo, onedrive +aad, defender, teams, exo, sharepoint +aad, defender, teams, onedrive, exo +aad, defender, teams, onedrive, sharepoint +aad, defender, teams, sharepoint, exo +aad, defender, teams, sharepoint, onedrive +aad, exo, defender, onedrive, sharepoint +aad, exo, defender, onedrive, teams +aad, exo, defender, sharepoint, onedrive +aad, exo, defender, sharepoint, teams +aad, exo, defender, teams, onedrive +aad, exo, defender, teams, sharepoint +aad, exo, onedrive, defender, sharepoint +aad, exo, onedrive, defender, teams +aad, exo, onedrive, sharepoint, defender +aad, exo, onedrive, sharepoint, teams +aad, exo, onedrive, teams, defender +aad, exo, onedrive, teams, sharepoint +aad, exo, sharepoint, defender, onedrive +aad, exo, sharepoint, defender, teams +aad, exo, sharepoint, onedrive, defender +aad, exo, sharepoint, onedrive, teams +aad, exo, sharepoint, teams, defender +aad, exo, sharepoint, teams, onedrive +aad, exo, teams, defender, onedrive +aad, exo, teams, defender, sharepoint +aad, exo, teams, onedrive, defender +aad, exo, teams, onedrive, sharepoint +aad, exo, teams, sharepoint, defender +aad, exo, teams, sharepoint, onedrive +aad, onedrive, defender, exo, sharepoint +aad, onedrive, defender, exo, teams +aad, onedrive, defender, sharepoint, exo +aad, onedrive, defender, sharepoint, teams +aad, onedrive, defender, teams, exo +aad, onedrive, defender, teams, sharepoint +aad, onedrive, exo, defender, sharepoint +aad, onedrive, exo, defender, teams +aad, onedrive, exo, sharepoint, defender +aad, onedrive, exo, sharepoint, teams +aad, onedrive, exo, teams, defender +aad, onedrive, exo, teams, sharepoint +aad, onedrive, sharepoint, defender, exo +aad, onedrive, sharepoint, defender, teams +aad, onedrive, sharepoint, exo, defender +aad, onedrive, sharepoint, exo, teams +aad, onedrive, sharepoint, teams, defender +aad, onedrive, sharepoint, teams, exo +aad, onedrive, teams, defender, exo +aad, onedrive, teams, defender, sharepoint +aad, onedrive, teams, exo, defender +aad, onedrive, teams, exo, sharepoint +aad, onedrive, teams, sharepoint, defender +aad, onedrive, teams, sharepoint, exo +aad, sharepoint, defender, exo, onedrive +aad, sharepoint, defender, exo, teams +aad, sharepoint, defender, onedrive, exo +aad, sharepoint, defender, onedrive, teams +aad, sharepoint, defender, teams, exo +aad, sharepoint, defender, teams, onedrive +aad, sharepoint, exo, defender, onedrive +aad, sharepoint, exo, defender, teams +aad, sharepoint, exo, onedrive, defender +aad, sharepoint, exo, onedrive, teams +aad, sharepoint, exo, teams, defender +aad, sharepoint, exo, teams, onedrive +aad, sharepoint, onedrive, defender, exo +aad, sharepoint, onedrive, defender, teams +aad, sharepoint, onedrive, exo, defender +aad, sharepoint, onedrive, exo, teams +aad, sharepoint, onedrive, teams, defender +aad, sharepoint, onedrive, teams, exo +aad, sharepoint, teams, defender, exo +aad, sharepoint, teams, defender, onedrive +aad, sharepoint, teams, exo, defender +aad, sharepoint, teams, exo, onedrive +aad, sharepoint, teams, onedrive, defender +aad, sharepoint, teams, onedrive, exo +aad, teams, defender, exo, onedrive +aad, teams, defender, exo, sharepoint +aad, teams, defender, onedrive, exo +aad, teams, defender, onedrive, sharepoint +aad, teams, defender, sharepoint, exo +aad, teams, defender, sharepoint, onedrive +aad, teams, exo, defender, onedrive +aad, teams, exo, defender, sharepoint +aad, teams, exo, onedrive, defender +aad, teams, exo, onedrive, sharepoint +aad, teams, exo, sharepoint, defender +aad, teams, exo, sharepoint, onedrive +aad, teams, onedrive, defender, exo +aad, teams, onedrive, defender, sharepoint +aad, teams, onedrive, exo, defender +aad, teams, onedrive, exo, sharepoint +aad, teams, onedrive, sharepoint, defender +aad, teams, onedrive, sharepoint, exo +aad, teams, sharepoint, defender, exo +aad, teams, sharepoint, defender, onedrive +aad, teams, sharepoint, exo, defender +aad, teams, sharepoint, exo, onedrive +aad, teams, sharepoint, onedrive, defender +aad, teams, sharepoint, onedrive, exo +defender, aad, exo, onedrive, sharepoint +defender, aad, exo, onedrive, teams +defender, aad, exo, sharepoint, onedrive +defender, aad, exo, sharepoint, teams +defender, aad, exo, teams, onedrive +defender, aad, exo, teams, sharepoint +defender, aad, onedrive, exo, sharepoint +defender, aad, onedrive, exo, teams +defender, aad, onedrive, sharepoint, exo +defender, aad, onedrive, sharepoint, teams +defender, aad, onedrive, teams, exo +defender, aad, onedrive, teams, sharepoint +defender, aad, sharepoint, exo, onedrive +defender, aad, sharepoint, exo, teams +defender, aad, sharepoint, onedrive, exo +defender, aad, sharepoint, onedrive, teams +defender, aad, sharepoint, teams, exo +defender, aad, sharepoint, teams, onedrive +defender, aad, teams, exo, onedrive +defender, aad, teams, exo, sharepoint +defender, aad, teams, onedrive, exo +defender, aad, teams, onedrive, sharepoint +defender, aad, teams, sharepoint, exo +defender, aad, teams, sharepoint, onedrive +defender, exo, aad, onedrive, sharepoint +defender, exo, aad, onedrive, teams +defender, exo, aad, sharepoint, onedrive +defender, exo, aad, sharepoint, teams +defender, exo, aad, teams, onedrive +defender, exo, aad, teams, sharepoint +defender, exo, onedrive, aad, sharepoint +defender, exo, onedrive, aad, teams +defender, exo, onedrive, sharepoint, aad +defender, exo, onedrive, sharepoint, teams +defender, exo, onedrive, teams, aad +defender, exo, onedrive, teams, sharepoint +defender, exo, sharepoint, aad, onedrive +defender, exo, sharepoint, aad, teams +defender, exo, sharepoint, onedrive, aad +defender, exo, sharepoint, onedrive, teams +defender, exo, sharepoint, teams, aad +defender, exo, sharepoint, teams, onedrive +defender, exo, teams, aad, onedrive +defender, exo, teams, aad, sharepoint +defender, exo, teams, onedrive, aad +defender, exo, teams, onedrive, sharepoint +defender, exo, teams, sharepoint, aad +defender, exo, teams, sharepoint, onedrive +defender, onedrive, aad, exo, sharepoint +defender, onedrive, aad, exo, teams +defender, onedrive, aad, sharepoint, exo +defender, onedrive, aad, sharepoint, teams +defender, onedrive, aad, teams, exo +defender, onedrive, aad, teams, sharepoint +defender, onedrive, exo, aad, sharepoint +defender, onedrive, exo, aad, teams +defender, onedrive, exo, sharepoint, aad +defender, onedrive, exo, sharepoint, teams +defender, onedrive, exo, teams, aad +defender, onedrive, exo, teams, sharepoint +defender, onedrive, sharepoint, aad, exo +defender, onedrive, sharepoint, aad, teams +defender, onedrive, sharepoint, exo, aad +defender, onedrive, sharepoint, exo, teams +defender, onedrive, sharepoint, teams, aad +defender, onedrive, sharepoint, teams, exo +defender, onedrive, teams, aad, exo +defender, onedrive, teams, aad, sharepoint +defender, onedrive, teams, exo, aad +defender, onedrive, teams, exo, sharepoint +defender, onedrive, teams, sharepoint, aad +defender, onedrive, teams, sharepoint, exo +defender, sharepoint, aad, exo, onedrive +defender, sharepoint, aad, exo, teams +defender, sharepoint, aad, onedrive, exo +defender, sharepoint, aad, onedrive, teams +defender, sharepoint, aad, teams, exo +defender, sharepoint, aad, teams, onedrive +defender, sharepoint, exo, aad, onedrive +defender, sharepoint, exo, aad, teams +defender, sharepoint, exo, onedrive, aad +defender, sharepoint, exo, onedrive, teams +defender, sharepoint, exo, teams, aad +defender, sharepoint, exo, teams, onedrive +defender, sharepoint, onedrive, aad, exo +defender, sharepoint, onedrive, aad, teams +defender, sharepoint, onedrive, exo, aad +defender, sharepoint, onedrive, exo, teams +defender, sharepoint, onedrive, teams, aad +defender, sharepoint, onedrive, teams, exo +defender, sharepoint, teams, aad, exo +defender, sharepoint, teams, aad, onedrive +defender, sharepoint, teams, exo, aad +defender, sharepoint, teams, exo, onedrive +defender, sharepoint, teams, onedrive, aad +defender, sharepoint, teams, onedrive, exo +defender, teams, aad, exo, onedrive +defender, teams, aad, exo, sharepoint +defender, teams, aad, onedrive, exo +defender, teams, aad, onedrive, sharepoint +defender, teams, aad, sharepoint, exo +defender, teams, aad, sharepoint, onedrive +defender, teams, exo, aad, onedrive +defender, teams, exo, aad, sharepoint +defender, teams, exo, onedrive, aad +defender, teams, exo, onedrive, sharepoint +defender, teams, exo, sharepoint, aad +defender, teams, exo, sharepoint, onedrive +defender, teams, onedrive, aad, exo +defender, teams, onedrive, aad, sharepoint +defender, teams, onedrive, exo, aad +defender, teams, onedrive, exo, sharepoint +defender, teams, onedrive, sharepoint, aad +defender, teams, onedrive, sharepoint, exo +defender, teams, sharepoint, aad, exo +defender, teams, sharepoint, aad, onedrive +defender, teams, sharepoint, exo, aad +defender, teams, sharepoint, exo, onedrive +defender, teams, sharepoint, onedrive, aad +defender, teams, sharepoint, onedrive, exo +exo, aad, defender, onedrive, sharepoint +exo, aad, defender, onedrive, teams +exo, aad, defender, sharepoint, onedrive +exo, aad, defender, sharepoint, teams +exo, aad, defender, teams, onedrive +exo, aad, defender, teams, sharepoint +exo, aad, onedrive, defender, sharepoint +exo, aad, onedrive, defender, teams +exo, aad, onedrive, sharepoint, defender +exo, aad, onedrive, sharepoint, teams +exo, aad, onedrive, teams, defender +exo, aad, onedrive, teams, sharepoint +exo, aad, sharepoint, defender, onedrive +exo, aad, sharepoint, defender, teams +exo, aad, sharepoint, onedrive, defender +exo, aad, sharepoint, onedrive, teams +exo, aad, sharepoint, teams, defender +exo, aad, sharepoint, teams, onedrive +exo, aad, teams, defender, onedrive +exo, aad, teams, defender, sharepoint +exo, aad, teams, onedrive, defender +exo, aad, teams, onedrive, sharepoint +exo, aad, teams, sharepoint, defender +exo, aad, teams, sharepoint, onedrive +exo, defender, aad, onedrive, sharepoint +exo, defender, aad, onedrive, teams +exo, defender, aad, sharepoint, onedrive +exo, defender, aad, sharepoint, teams +exo, defender, aad, teams, onedrive +exo, defender, aad, teams, sharepoint +exo, defender, onedrive, aad, sharepoint +exo, defender, onedrive, aad, teams +exo, defender, onedrive, sharepoint, aad +exo, defender, onedrive, sharepoint, teams +exo, defender, onedrive, teams, aad +exo, defender, onedrive, teams, sharepoint +exo, defender, sharepoint, aad, onedrive +exo, defender, sharepoint, aad, teams +exo, defender, sharepoint, onedrive, aad +exo, defender, sharepoint, onedrive, teams +exo, defender, sharepoint, teams, aad +exo, defender, sharepoint, teams, onedrive +exo, defender, teams, aad, onedrive +exo, defender, teams, aad, sharepoint +exo, defender, teams, onedrive, aad +exo, defender, teams, onedrive, sharepoint +exo, defender, teams, sharepoint, aad +exo, defender, teams, sharepoint, onedrive +exo, onedrive, aad, defender, sharepoint +exo, onedrive, aad, defender, teams +exo, onedrive, aad, sharepoint, defender +exo, onedrive, aad, sharepoint, teams +exo, onedrive, aad, teams, defender +exo, onedrive, aad, teams, sharepoint +exo, onedrive, defender, aad, sharepoint +exo, onedrive, defender, aad, teams +exo, onedrive, defender, sharepoint, aad +exo, onedrive, defender, sharepoint, teams +exo, onedrive, defender, teams, aad +exo, onedrive, defender, teams, sharepoint +exo, onedrive, sharepoint, aad, defender +exo, onedrive, sharepoint, aad, teams +exo, onedrive, sharepoint, defender, aad +exo, onedrive, sharepoint, defender, teams +exo, onedrive, sharepoint, teams, aad +exo, onedrive, sharepoint, teams, defender +exo, onedrive, teams, aad, defender +exo, onedrive, teams, aad, sharepoint +exo, onedrive, teams, defender, aad +exo, onedrive, teams, defender, sharepoint +exo, onedrive, teams, sharepoint, aad +exo, onedrive, teams, sharepoint, defender +exo, sharepoint, aad, defender, onedrive +exo, sharepoint, aad, defender, teams +exo, sharepoint, aad, onedrive, defender +exo, sharepoint, aad, onedrive, teams +exo, sharepoint, aad, teams, defender +exo, sharepoint, aad, teams, onedrive +exo, sharepoint, defender, aad, onedrive +exo, sharepoint, defender, aad, teams +exo, sharepoint, defender, onedrive, aad +exo, sharepoint, defender, onedrive, teams +exo, sharepoint, defender, teams, aad +exo, sharepoint, defender, teams, onedrive +exo, sharepoint, onedrive, aad, defender +exo, sharepoint, onedrive, aad, teams +exo, sharepoint, onedrive, defender, aad +exo, sharepoint, onedrive, defender, teams +exo, sharepoint, onedrive, teams, aad +exo, sharepoint, onedrive, teams, defender +exo, sharepoint, teams, aad, defender +exo, sharepoint, teams, aad, onedrive +exo, sharepoint, teams, defender, aad +exo, sharepoint, teams, defender, onedrive +exo, sharepoint, teams, onedrive, aad +exo, sharepoint, teams, onedrive, defender +exo, teams, aad, defender, onedrive +exo, teams, aad, defender, sharepoint +exo, teams, aad, onedrive, defender +exo, teams, aad, onedrive, sharepoint +exo, teams, aad, sharepoint, defender +exo, teams, aad, sharepoint, onedrive +exo, teams, defender, aad, onedrive +exo, teams, defender, aad, sharepoint +exo, teams, defender, onedrive, aad +exo, teams, defender, onedrive, sharepoint +exo, teams, defender, sharepoint, aad +exo, teams, defender, sharepoint, onedrive +exo, teams, onedrive, aad, defender +exo, teams, onedrive, aad, sharepoint +exo, teams, onedrive, defender, aad +exo, teams, onedrive, defender, sharepoint +exo, teams, onedrive, sharepoint, aad +exo, teams, onedrive, sharepoint, defender +exo, teams, sharepoint, aad, defender +exo, teams, sharepoint, aad, onedrive +exo, teams, sharepoint, defender, aad +exo, teams, sharepoint, defender, onedrive +exo, teams, sharepoint, onedrive, aad +exo, teams, sharepoint, onedrive, defender +onedrive, aad, defender, exo, sharepoint +onedrive, aad, defender, exo, teams +onedrive, aad, defender, sharepoint, exo +onedrive, aad, defender, sharepoint, teams +onedrive, aad, defender, teams, exo +onedrive, aad, defender, teams, sharepoint +onedrive, aad, exo, defender, sharepoint +onedrive, aad, exo, defender, teams +onedrive, aad, exo, sharepoint, defender +onedrive, aad, exo, sharepoint, teams +onedrive, aad, exo, teams, defender +onedrive, aad, exo, teams, sharepoint +onedrive, aad, sharepoint, defender, exo +onedrive, aad, sharepoint, defender, teams +onedrive, aad, sharepoint, exo, defender +onedrive, aad, sharepoint, exo, teams +onedrive, aad, sharepoint, teams, defender +onedrive, aad, sharepoint, teams, exo +onedrive, aad, teams, defender, exo +onedrive, aad, teams, defender, sharepoint +onedrive, aad, teams, exo, defender +onedrive, aad, teams, exo, sharepoint +onedrive, aad, teams, sharepoint, defender +onedrive, aad, teams, sharepoint, exo +onedrive, defender, aad, exo, sharepoint +onedrive, defender, aad, exo, teams +onedrive, defender, aad, sharepoint, exo +onedrive, defender, aad, sharepoint, teams +onedrive, defender, aad, teams, exo +onedrive, defender, aad, teams, sharepoint +onedrive, defender, exo, aad, sharepoint +onedrive, defender, exo, aad, teams +onedrive, defender, exo, sharepoint, aad +onedrive, defender, exo, sharepoint, teams +onedrive, defender, exo, teams, aad +onedrive, defender, exo, teams, sharepoint +onedrive, defender, sharepoint, aad, exo +onedrive, defender, sharepoint, aad, teams +onedrive, defender, sharepoint, exo, aad +onedrive, defender, sharepoint, exo, teams +onedrive, defender, sharepoint, teams, aad +onedrive, defender, sharepoint, teams, exo +onedrive, defender, teams, aad, exo +onedrive, defender, teams, aad, sharepoint +onedrive, defender, teams, exo, aad +onedrive, defender, teams, exo, sharepoint +onedrive, defender, teams, sharepoint, aad +onedrive, defender, teams, sharepoint, exo +onedrive, exo, aad, defender, sharepoint +onedrive, exo, aad, defender, teams +onedrive, exo, aad, sharepoint, defender +onedrive, exo, aad, sharepoint, teams +onedrive, exo, aad, teams, defender +onedrive, exo, aad, teams, sharepoint +onedrive, exo, defender, aad, sharepoint +onedrive, exo, defender, aad, teams +onedrive, exo, defender, sharepoint, aad +onedrive, exo, defender, sharepoint, teams +onedrive, exo, defender, teams, aad +onedrive, exo, defender, teams, sharepoint +onedrive, exo, sharepoint, aad, defender +onedrive, exo, sharepoint, aad, teams +onedrive, exo, sharepoint, defender, aad +onedrive, exo, sharepoint, defender, teams +onedrive, exo, sharepoint, teams, aad +onedrive, exo, sharepoint, teams, defender +onedrive, exo, teams, aad, defender +onedrive, exo, teams, aad, sharepoint +onedrive, exo, teams, defender, aad +onedrive, exo, teams, defender, sharepoint +onedrive, exo, teams, sharepoint, aad +onedrive, exo, teams, sharepoint, defender +onedrive, sharepoint, aad, defender, exo +onedrive, sharepoint, aad, defender, teams +onedrive, sharepoint, aad, exo, defender +onedrive, sharepoint, aad, exo, teams +onedrive, sharepoint, aad, teams, defender +onedrive, sharepoint, aad, teams, exo +onedrive, sharepoint, defender, aad, exo +onedrive, sharepoint, defender, aad, teams +onedrive, sharepoint, defender, exo, aad +onedrive, sharepoint, defender, exo, teams +onedrive, sharepoint, defender, teams, aad +onedrive, sharepoint, defender, teams, exo +onedrive, sharepoint, exo, aad, defender +onedrive, sharepoint, exo, aad, teams +onedrive, sharepoint, exo, defender, aad +onedrive, sharepoint, exo, defender, teams +onedrive, sharepoint, exo, teams, aad +onedrive, sharepoint, exo, teams, defender +onedrive, sharepoint, teams, aad, defender +onedrive, sharepoint, teams, aad, exo +onedrive, sharepoint, teams, defender, aad +onedrive, sharepoint, teams, defender, exo +onedrive, sharepoint, teams, exo, aad +onedrive, sharepoint, teams, exo, defender +onedrive, teams, aad, defender, exo +onedrive, teams, aad, defender, sharepoint +onedrive, teams, aad, exo, defender +onedrive, teams, aad, exo, sharepoint +onedrive, teams, aad, sharepoint, defender +onedrive, teams, aad, sharepoint, exo +onedrive, teams, defender, aad, exo +onedrive, teams, defender, aad, sharepoint +onedrive, teams, defender, exo, aad +onedrive, teams, defender, exo, sharepoint +onedrive, teams, defender, sharepoint, aad +onedrive, teams, defender, sharepoint, exo +onedrive, teams, exo, aad, defender +onedrive, teams, exo, aad, sharepoint +onedrive, teams, exo, defender, aad +onedrive, teams, exo, defender, sharepoint +onedrive, teams, exo, sharepoint, aad +onedrive, teams, exo, sharepoint, defender +onedrive, teams, sharepoint, aad, defender +onedrive, teams, sharepoint, aad, exo +onedrive, teams, sharepoint, defender, aad +onedrive, teams, sharepoint, defender, exo +onedrive, teams, sharepoint, exo, aad +onedrive, teams, sharepoint, exo, defender +sharepoint, aad, defender, exo, onedrive +sharepoint, aad, defender, exo, teams +sharepoint, aad, defender, onedrive, exo +sharepoint, aad, defender, onedrive, teams +sharepoint, aad, defender, teams, exo +sharepoint, aad, defender, teams, onedrive +sharepoint, aad, exo, defender, onedrive +sharepoint, aad, exo, defender, teams +sharepoint, aad, exo, onedrive, defender +sharepoint, aad, exo, onedrive, teams +sharepoint, aad, exo, teams, defender +sharepoint, aad, exo, teams, onedrive +sharepoint, aad, onedrive, defender, exo +sharepoint, aad, onedrive, defender, teams +sharepoint, aad, onedrive, exo, defender +sharepoint, aad, onedrive, exo, teams +sharepoint, aad, onedrive, teams, defender +sharepoint, aad, onedrive, teams, exo +sharepoint, aad, teams, defender, exo +sharepoint, aad, teams, defender, onedrive +sharepoint, aad, teams, exo, defender +sharepoint, aad, teams, exo, onedrive +sharepoint, aad, teams, onedrive, defender +sharepoint, aad, teams, onedrive, exo +sharepoint, defender, aad, exo, onedrive +sharepoint, defender, aad, exo, teams +sharepoint, defender, aad, onedrive, exo +sharepoint, defender, aad, onedrive, teams +sharepoint, defender, aad, teams, exo +sharepoint, defender, aad, teams, onedrive +sharepoint, defender, exo, aad, onedrive +sharepoint, defender, exo, aad, teams +sharepoint, defender, exo, onedrive, aad +sharepoint, defender, exo, onedrive, teams +sharepoint, defender, exo, teams, aad +sharepoint, defender, exo, teams, onedrive +sharepoint, defender, onedrive, aad, exo +sharepoint, defender, onedrive, aad, teams +sharepoint, defender, onedrive, exo, aad +sharepoint, defender, onedrive, exo, teams +sharepoint, defender, onedrive, teams, aad +sharepoint, defender, onedrive, teams, exo +sharepoint, defender, teams, aad, exo +sharepoint, defender, teams, aad, onedrive +sharepoint, defender, teams, exo, aad +sharepoint, defender, teams, exo, onedrive +sharepoint, defender, teams, onedrive, aad +sharepoint, defender, teams, onedrive, exo +sharepoint, exo, aad, defender, onedrive +sharepoint, exo, aad, defender, teams +sharepoint, exo, aad, onedrive, defender +sharepoint, exo, aad, onedrive, teams +sharepoint, exo, aad, teams, defender +sharepoint, exo, aad, teams, onedrive +sharepoint, exo, defender, aad, onedrive +sharepoint, exo, defender, aad, teams +sharepoint, exo, defender, onedrive, aad +sharepoint, exo, defender, onedrive, teams +sharepoint, exo, defender, teams, aad +sharepoint, exo, defender, teams, onedrive +sharepoint, exo, onedrive, aad, defender +sharepoint, exo, onedrive, aad, teams +sharepoint, exo, onedrive, defender, aad +sharepoint, exo, onedrive, defender, teams +sharepoint, exo, onedrive, teams, aad +sharepoint, exo, onedrive, teams, defender +sharepoint, exo, teams, aad, defender +sharepoint, exo, teams, aad, onedrive +sharepoint, exo, teams, defender, aad +sharepoint, exo, teams, defender, onedrive +sharepoint, exo, teams, onedrive, aad +sharepoint, exo, teams, onedrive, defender +sharepoint, onedrive, aad, defender, exo +sharepoint, onedrive, aad, defender, teams +sharepoint, onedrive, aad, exo, defender +sharepoint, onedrive, aad, exo, teams +sharepoint, onedrive, aad, teams, defender +sharepoint, onedrive, aad, teams, exo +sharepoint, onedrive, defender, aad, exo +sharepoint, onedrive, defender, aad, teams +sharepoint, onedrive, defender, exo, aad +sharepoint, onedrive, defender, exo, teams +sharepoint, onedrive, defender, teams, aad +sharepoint, onedrive, defender, teams, exo +sharepoint, onedrive, exo, aad, defender +sharepoint, onedrive, exo, aad, teams +sharepoint, onedrive, exo, defender, aad +sharepoint, onedrive, exo, defender, teams +sharepoint, onedrive, exo, teams, aad +sharepoint, onedrive, exo, teams, defender +sharepoint, onedrive, teams, aad, defender +sharepoint, onedrive, teams, aad, exo +sharepoint, onedrive, teams, defender, aad +sharepoint, onedrive, teams, defender, exo +sharepoint, onedrive, teams, exo, aad +sharepoint, onedrive, teams, exo, defender +sharepoint, teams, aad, defender, exo +sharepoint, teams, aad, defender, onedrive +sharepoint, teams, aad, exo, defender +sharepoint, teams, aad, exo, onedrive +sharepoint, teams, aad, onedrive, defender +sharepoint, teams, aad, onedrive, exo +sharepoint, teams, defender, aad, exo +sharepoint, teams, defender, aad, onedrive +sharepoint, teams, defender, exo, aad +sharepoint, teams, defender, exo, onedrive +sharepoint, teams, defender, onedrive, aad +sharepoint, teams, defender, onedrive, exo +sharepoint, teams, exo, aad, defender +sharepoint, teams, exo, aad, onedrive +sharepoint, teams, exo, defender, aad +sharepoint, teams, exo, defender, onedrive +sharepoint, teams, exo, onedrive, aad +sharepoint, teams, exo, onedrive, defender +sharepoint, teams, onedrive, aad, defender +sharepoint, teams, onedrive, aad, exo +sharepoint, teams, onedrive, defender, aad +sharepoint, teams, onedrive, defender, exo +sharepoint, teams, onedrive, exo, aad +sharepoint, teams, onedrive, exo, defender +teams, aad, defender, exo, onedrive +teams, aad, defender, exo, sharepoint +teams, aad, defender, onedrive, exo +teams, aad, defender, onedrive, sharepoint +teams, aad, defender, sharepoint, exo +teams, aad, defender, sharepoint, onedrive +teams, aad, exo, defender, onedrive +teams, aad, exo, defender, sharepoint +teams, aad, exo, onedrive, defender +teams, aad, exo, onedrive, sharepoint +teams, aad, exo, sharepoint, defender +teams, aad, exo, sharepoint, onedrive +teams, aad, onedrive, defender, exo +teams, aad, onedrive, defender, sharepoint +teams, aad, onedrive, exo, defender +teams, aad, onedrive, exo, sharepoint +teams, aad, onedrive, sharepoint, defender +teams, aad, onedrive, sharepoint, exo +teams, aad, sharepoint, defender, exo +teams, aad, sharepoint, defender, onedrive +teams, aad, sharepoint, exo, defender +teams, aad, sharepoint, exo, onedrive +teams, aad, sharepoint, onedrive, defender +teams, aad, sharepoint, onedrive, exo +teams, defender, aad, exo, onedrive +teams, defender, aad, exo, sharepoint +teams, defender, aad, onedrive, exo +teams, defender, aad, onedrive, sharepoint +teams, defender, aad, sharepoint, exo +teams, defender, aad, sharepoint, onedrive +teams, defender, exo, aad, onedrive +teams, defender, exo, aad, sharepoint +teams, defender, exo, onedrive, aad +teams, defender, exo, onedrive, sharepoint +teams, defender, exo, sharepoint, aad +teams, defender, exo, sharepoint, onedrive +teams, defender, onedrive, aad, exo +teams, defender, onedrive, aad, sharepoint +teams, defender, onedrive, exo, aad +teams, defender, onedrive, exo, sharepoint +teams, defender, onedrive, sharepoint, aad +teams, defender, onedrive, sharepoint, exo +teams, defender, sharepoint, aad, exo +teams, defender, sharepoint, aad, onedrive +teams, defender, sharepoint, exo, aad +teams, defender, sharepoint, exo, onedrive +teams, defender, sharepoint, onedrive, aad +teams, defender, sharepoint, onedrive, exo +teams, exo, aad, defender, onedrive +teams, exo, aad, defender, sharepoint +teams, exo, aad, onedrive, defender +teams, exo, aad, onedrive, sharepoint +teams, exo, aad, sharepoint, defender +teams, exo, aad, sharepoint, onedrive +teams, exo, defender, aad, onedrive +teams, exo, defender, aad, sharepoint +teams, exo, defender, onedrive, aad +teams, exo, defender, onedrive, sharepoint +teams, exo, defender, sharepoint, aad +teams, exo, defender, sharepoint, onedrive +teams, exo, onedrive, aad, defender +teams, exo, onedrive, aad, sharepoint +teams, exo, onedrive, defender, aad +teams, exo, onedrive, defender, sharepoint +teams, exo, onedrive, sharepoint, aad +teams, exo, onedrive, sharepoint, defender +teams, exo, sharepoint, aad, defender +teams, exo, sharepoint, aad, onedrive +teams, exo, sharepoint, defender, aad +teams, exo, sharepoint, defender, onedrive +teams, exo, sharepoint, onedrive, aad +teams, exo, sharepoint, onedrive, defender +teams, onedrive, aad, defender, exo +teams, onedrive, aad, defender, sharepoint +teams, onedrive, aad, exo, defender +teams, onedrive, aad, exo, sharepoint +teams, onedrive, aad, sharepoint, defender +teams, onedrive, aad, sharepoint, exo +teams, onedrive, defender, aad, exo +teams, onedrive, defender, aad, sharepoint +teams, onedrive, defender, exo, aad +teams, onedrive, defender, exo, sharepoint +teams, onedrive, defender, sharepoint, aad +teams, onedrive, defender, sharepoint, exo +teams, onedrive, exo, aad, defender +teams, onedrive, exo, aad, sharepoint +teams, onedrive, exo, defender, aad +teams, onedrive, exo, defender, sharepoint +teams, onedrive, exo, sharepoint, aad +teams, onedrive, exo, sharepoint, defender +teams, onedrive, sharepoint, aad, defender +teams, onedrive, sharepoint, aad, exo +teams, onedrive, sharepoint, defender, aad +teams, onedrive, sharepoint, defender, exo +teams, onedrive, sharepoint, exo, aad +teams, onedrive, sharepoint, exo, defender +teams, sharepoint, aad, defender, exo +teams, sharepoint, aad, defender, onedrive +teams, sharepoint, aad, exo, defender +teams, sharepoint, aad, exo, onedrive +teams, sharepoint, aad, onedrive, defender +teams, sharepoint, aad, onedrive, exo +teams, sharepoint, defender, aad, exo +teams, sharepoint, defender, aad, onedrive +teams, sharepoint, defender, exo, aad +teams, sharepoint, defender, exo, onedrive +teams, sharepoint, defender, onedrive, aad +teams, sharepoint, defender, onedrive, exo +teams, sharepoint, exo, aad, defender +teams, sharepoint, exo, aad, onedrive +teams, sharepoint, exo, defender, aad +teams, sharepoint, exo, defender, onedrive +teams, sharepoint, exo, onedrive, aad +teams, sharepoint, exo, onedrive, defender +teams, sharepoint, onedrive, aad, defender +teams, sharepoint, onedrive, aad, exo +teams, sharepoint, onedrive, defender, aad +teams, sharepoint, onedrive, defender, exo +teams, sharepoint, onedrive, exo, aad +teams, sharepoint, onedrive, exo, defender +aad, defender, exo, onedrive, sharepoint, teams +aad, defender, exo, onedrive, teams, sharepoint +aad, defender, exo, sharepoint, onedrive, teams +aad, defender, exo, sharepoint, teams, onedrive +aad, defender, exo, teams, onedrive, sharepoint +aad, defender, exo, teams, sharepoint, onedrive +aad, defender, onedrive, exo, sharepoint, teams +aad, defender, onedrive, exo, teams, sharepoint +aad, defender, onedrive, sharepoint, exo, teams +aad, defender, onedrive, sharepoint, teams, exo +aad, defender, onedrive, teams, exo, sharepoint +aad, defender, onedrive, teams, sharepoint, exo +aad, defender, sharepoint, exo, onedrive, teams +aad, defender, sharepoint, exo, teams, onedrive +aad, defender, sharepoint, onedrive, exo, teams +aad, defender, sharepoint, onedrive, teams, exo +aad, defender, sharepoint, teams, exo, onedrive +aad, defender, sharepoint, teams, onedrive, exo +aad, defender, teams, exo, onedrive, sharepoint +aad, defender, teams, exo, sharepoint, onedrive +aad, defender, teams, onedrive, exo, sharepoint +aad, defender, teams, onedrive, sharepoint, exo +aad, defender, teams, sharepoint, exo, onedrive +aad, defender, teams, sharepoint, onedrive, exo +aad, exo, defender, onedrive, sharepoint, teams +aad, exo, defender, onedrive, teams, sharepoint +aad, exo, defender, sharepoint, onedrive, teams +aad, exo, defender, sharepoint, teams, onedrive +aad, exo, defender, teams, onedrive, sharepoint +aad, exo, defender, teams, sharepoint, onedrive +aad, exo, onedrive, defender, sharepoint, teams +aad, exo, onedrive, defender, teams, sharepoint +aad, exo, onedrive, sharepoint, defender, teams +aad, exo, onedrive, sharepoint, teams, defender +aad, exo, onedrive, teams, defender, sharepoint +aad, exo, onedrive, teams, sharepoint, defender +aad, exo, sharepoint, defender, onedrive, teams +aad, exo, sharepoint, defender, teams, onedrive +aad, exo, sharepoint, onedrive, defender, teams +aad, exo, sharepoint, onedrive, teams, defender +aad, exo, sharepoint, teams, defender, onedrive +aad, exo, sharepoint, teams, onedrive, defender +aad, exo, teams, defender, onedrive, sharepoint +aad, exo, teams, defender, sharepoint, onedrive +aad, exo, teams, onedrive, defender, sharepoint +aad, exo, teams, onedrive, sharepoint, defender +aad, exo, teams, sharepoint, defender, onedrive +aad, exo, teams, sharepoint, onedrive, defender +aad, onedrive, defender, exo, sharepoint, teams +aad, onedrive, defender, exo, teams, sharepoint +aad, onedrive, defender, sharepoint, exo, teams +aad, onedrive, defender, sharepoint, teams, exo +aad, onedrive, defender, teams, exo, sharepoint +aad, onedrive, defender, teams, sharepoint, exo +aad, onedrive, exo, defender, sharepoint, teams +aad, onedrive, exo, defender, teams, sharepoint +aad, onedrive, exo, sharepoint, defender, teams +aad, onedrive, exo, sharepoint, teams, defender +aad, onedrive, exo, teams, defender, sharepoint +aad, onedrive, exo, teams, sharepoint, defender +aad, onedrive, sharepoint, defender, exo, teams +aad, onedrive, sharepoint, defender, teams, exo +aad, onedrive, sharepoint, exo, defender, teams +aad, onedrive, sharepoint, exo, teams, defender +aad, onedrive, sharepoint, teams, defender, exo +aad, onedrive, sharepoint, teams, exo, defender +aad, onedrive, teams, defender, exo, sharepoint +aad, onedrive, teams, defender, sharepoint, exo +aad, onedrive, teams, exo, defender, sharepoint +aad, onedrive, teams, exo, sharepoint, defender +aad, onedrive, teams, sharepoint, defender, exo +aad, onedrive, teams, sharepoint, exo, defender +aad, sharepoint, defender, exo, onedrive, teams +aad, sharepoint, defender, exo, teams, onedrive +aad, sharepoint, defender, onedrive, exo, teams +aad, sharepoint, defender, onedrive, teams, exo +aad, sharepoint, defender, teams, exo, onedrive +aad, sharepoint, defender, teams, onedrive, exo +aad, sharepoint, exo, defender, onedrive, teams +aad, sharepoint, exo, defender, teams, onedrive +aad, sharepoint, exo, onedrive, defender, teams +aad, sharepoint, exo, onedrive, teams, defender +aad, sharepoint, exo, teams, defender, onedrive +aad, sharepoint, exo, teams, onedrive, defender +aad, sharepoint, onedrive, defender, exo, teams +aad, sharepoint, onedrive, defender, teams, exo +aad, sharepoint, onedrive, exo, defender, teams +aad, sharepoint, onedrive, exo, teams, defender +aad, sharepoint, onedrive, teams, defender, exo +aad, sharepoint, onedrive, teams, exo, defender +aad, sharepoint, teams, defender, exo, onedrive +aad, sharepoint, teams, defender, onedrive, exo +aad, sharepoint, teams, exo, defender, onedrive +aad, sharepoint, teams, exo, onedrive, defender +aad, sharepoint, teams, onedrive, defender, exo +aad, sharepoint, teams, onedrive, exo, defender +aad, teams, defender, exo, onedrive, sharepoint +aad, teams, defender, exo, sharepoint, onedrive +aad, teams, defender, onedrive, exo, sharepoint +aad, teams, defender, onedrive, sharepoint, exo +aad, teams, defender, sharepoint, exo, onedrive +aad, teams, defender, sharepoint, onedrive, exo +aad, teams, exo, defender, onedrive, sharepoint +aad, teams, exo, defender, sharepoint, onedrive +aad, teams, exo, onedrive, defender, sharepoint +aad, teams, exo, onedrive, sharepoint, defender +aad, teams, exo, sharepoint, defender, onedrive +aad, teams, exo, sharepoint, onedrive, defender +aad, teams, onedrive, defender, exo, sharepoint +aad, teams, onedrive, defender, sharepoint, exo +aad, teams, onedrive, exo, defender, sharepoint +aad, teams, onedrive, exo, sharepoint, defender +aad, teams, onedrive, sharepoint, defender, exo +aad, teams, onedrive, sharepoint, exo, defender +aad, teams, sharepoint, defender, exo, onedrive +aad, teams, sharepoint, defender, onedrive, exo +aad, teams, sharepoint, exo, defender, onedrive +aad, teams, sharepoint, exo, onedrive, defender +aad, teams, sharepoint, onedrive, defender, exo +aad, teams, sharepoint, onedrive, exo, defender +defender, aad, exo, onedrive, sharepoint, teams +defender, aad, exo, onedrive, teams, sharepoint +defender, aad, exo, sharepoint, onedrive, teams +defender, aad, exo, sharepoint, teams, onedrive +defender, aad, exo, teams, onedrive, sharepoint +defender, aad, exo, teams, sharepoint, onedrive +defender, aad, onedrive, exo, sharepoint, teams +defender, aad, onedrive, exo, teams, sharepoint +defender, aad, onedrive, sharepoint, exo, teams +defender, aad, onedrive, sharepoint, teams, exo +defender, aad, onedrive, teams, exo, sharepoint +defender, aad, onedrive, teams, sharepoint, exo +defender, aad, sharepoint, exo, onedrive, teams +defender, aad, sharepoint, exo, teams, onedrive +defender, aad, sharepoint, onedrive, exo, teams +defender, aad, sharepoint, onedrive, teams, exo +defender, aad, sharepoint, teams, exo, onedrive +defender, aad, sharepoint, teams, onedrive, exo +defender, aad, teams, exo, onedrive, sharepoint +defender, aad, teams, exo, sharepoint, onedrive +defender, aad, teams, onedrive, exo, sharepoint +defender, aad, teams, onedrive, sharepoint, exo +defender, aad, teams, sharepoint, exo, onedrive +defender, aad, teams, sharepoint, onedrive, exo +defender, exo, aad, onedrive, sharepoint, teams +defender, exo, aad, onedrive, teams, sharepoint +defender, exo, aad, sharepoint, onedrive, teams +defender, exo, aad, sharepoint, teams, onedrive +defender, exo, aad, teams, onedrive, sharepoint +defender, exo, aad, teams, sharepoint, onedrive +defender, exo, onedrive, aad, sharepoint, teams +defender, exo, onedrive, aad, teams, sharepoint +defender, exo, onedrive, sharepoint, aad, teams +defender, exo, onedrive, sharepoint, teams, aad +defender, exo, onedrive, teams, aad, sharepoint +defender, exo, onedrive, teams, sharepoint, aad +defender, exo, sharepoint, aad, onedrive, teams +defender, exo, sharepoint, aad, teams, onedrive +defender, exo, sharepoint, onedrive, aad, teams +defender, exo, sharepoint, onedrive, teams, aad +defender, exo, sharepoint, teams, aad, onedrive +defender, exo, sharepoint, teams, onedrive, aad +defender, exo, teams, aad, onedrive, sharepoint +defender, exo, teams, aad, sharepoint, onedrive +defender, exo, teams, onedrive, aad, sharepoint +defender, exo, teams, onedrive, sharepoint, aad +defender, exo, teams, sharepoint, aad, onedrive +defender, exo, teams, sharepoint, onedrive, aad +defender, onedrive, aad, exo, sharepoint, teams +defender, onedrive, aad, exo, teams, sharepoint +defender, onedrive, aad, sharepoint, exo, teams +defender, onedrive, aad, sharepoint, teams, exo +defender, onedrive, aad, teams, exo, sharepoint +defender, onedrive, aad, teams, sharepoint, exo +defender, onedrive, exo, aad, sharepoint, teams +defender, onedrive, exo, aad, teams, sharepoint +defender, onedrive, exo, sharepoint, aad, teams +defender, onedrive, exo, sharepoint, teams, aad +defender, onedrive, exo, teams, aad, sharepoint +defender, onedrive, exo, teams, sharepoint, aad +defender, onedrive, sharepoint, aad, exo, teams +defender, onedrive, sharepoint, aad, teams, exo +defender, onedrive, sharepoint, exo, aad, teams +defender, onedrive, sharepoint, exo, teams, aad +defender, onedrive, sharepoint, teams, aad, exo +defender, onedrive, sharepoint, teams, exo, aad +defender, onedrive, teams, aad, exo, sharepoint +defender, onedrive, teams, aad, sharepoint, exo +defender, onedrive, teams, exo, aad, sharepoint +defender, onedrive, teams, exo, sharepoint, aad +defender, onedrive, teams, sharepoint, aad, exo +defender, onedrive, teams, sharepoint, exo, aad +defender, sharepoint, aad, exo, onedrive, teams +defender, sharepoint, aad, exo, teams, onedrive +defender, sharepoint, aad, onedrive, exo, teams +defender, sharepoint, aad, onedrive, teams, exo +defender, sharepoint, aad, teams, exo, onedrive +defender, sharepoint, aad, teams, onedrive, exo +defender, sharepoint, exo, aad, onedrive, teams +defender, sharepoint, exo, aad, teams, onedrive +defender, sharepoint, exo, onedrive, aad, teams +defender, sharepoint, exo, onedrive, teams, aad +defender, sharepoint, exo, teams, aad, onedrive +defender, sharepoint, exo, teams, onedrive, aad +defender, sharepoint, onedrive, aad, exo, teams +defender, sharepoint, onedrive, aad, teams, exo +defender, sharepoint, onedrive, exo, aad, teams +defender, sharepoint, onedrive, exo, teams, aad +defender, sharepoint, onedrive, teams, aad, exo +defender, sharepoint, onedrive, teams, exo, aad +defender, sharepoint, teams, aad, exo, onedrive +defender, sharepoint, teams, aad, onedrive, exo +defender, sharepoint, teams, exo, aad, onedrive +defender, sharepoint, teams, exo, onedrive, aad +defender, sharepoint, teams, onedrive, aad, exo +defender, sharepoint, teams, onedrive, exo, aad +defender, teams, aad, exo, onedrive, sharepoint +defender, teams, aad, exo, sharepoint, onedrive +defender, teams, aad, onedrive, exo, sharepoint +defender, teams, aad, onedrive, sharepoint, exo +defender, teams, aad, sharepoint, exo, onedrive +defender, teams, aad, sharepoint, onedrive, exo +defender, teams, exo, aad, onedrive, sharepoint +defender, teams, exo, aad, sharepoint, onedrive +defender, teams, exo, onedrive, aad, sharepoint +defender, teams, exo, onedrive, sharepoint, aad +defender, teams, exo, sharepoint, aad, onedrive +defender, teams, exo, sharepoint, onedrive, aad +defender, teams, onedrive, aad, exo, sharepoint +defender, teams, onedrive, aad, sharepoint, exo +defender, teams, onedrive, exo, aad, sharepoint +defender, teams, onedrive, exo, sharepoint, aad +defender, teams, onedrive, sharepoint, aad, exo +defender, teams, onedrive, sharepoint, exo, aad +defender, teams, sharepoint, aad, exo, onedrive +defender, teams, sharepoint, aad, onedrive, exo +defender, teams, sharepoint, exo, aad, onedrive +defender, teams, sharepoint, exo, onedrive, aad +defender, teams, sharepoint, onedrive, aad, exo +defender, teams, sharepoint, onedrive, exo, aad +exo, aad, defender, onedrive, sharepoint, teams +exo, aad, defender, onedrive, teams, sharepoint +exo, aad, defender, sharepoint, onedrive, teams +exo, aad, defender, sharepoint, teams, onedrive +exo, aad, defender, teams, onedrive, sharepoint +exo, aad, defender, teams, sharepoint, onedrive +exo, aad, onedrive, defender, sharepoint, teams +exo, aad, onedrive, defender, teams, sharepoint +exo, aad, onedrive, sharepoint, defender, teams +exo, aad, onedrive, sharepoint, teams, defender +exo, aad, onedrive, teams, defender, sharepoint +exo, aad, onedrive, teams, sharepoint, defender +exo, aad, sharepoint, defender, onedrive, teams +exo, aad, sharepoint, defender, teams, onedrive +exo, aad, sharepoint, onedrive, defender, teams +exo, aad, sharepoint, onedrive, teams, defender +exo, aad, sharepoint, teams, defender, onedrive +exo, aad, sharepoint, teams, onedrive, defender +exo, aad, teams, defender, onedrive, sharepoint +exo, aad, teams, defender, sharepoint, onedrive +exo, aad, teams, onedrive, defender, sharepoint +exo, aad, teams, onedrive, sharepoint, defender +exo, aad, teams, sharepoint, defender, onedrive +exo, aad, teams, sharepoint, onedrive, defender +exo, defender, aad, onedrive, sharepoint, teams +exo, defender, aad, onedrive, teams, sharepoint +exo, defender, aad, sharepoint, onedrive, teams +exo, defender, aad, sharepoint, teams, onedrive +exo, defender, aad, teams, onedrive, sharepoint +exo, defender, aad, teams, sharepoint, onedrive +exo, defender, onedrive, aad, sharepoint, teams +exo, defender, onedrive, aad, teams, sharepoint +exo, defender, onedrive, sharepoint, aad, teams +exo, defender, onedrive, sharepoint, teams, aad +exo, defender, onedrive, teams, aad, sharepoint +exo, defender, onedrive, teams, sharepoint, aad +exo, defender, sharepoint, aad, onedrive, teams +exo, defender, sharepoint, aad, teams, onedrive +exo, defender, sharepoint, onedrive, aad, teams +exo, defender, sharepoint, onedrive, teams, aad +exo, defender, sharepoint, teams, aad, onedrive +exo, defender, sharepoint, teams, onedrive, aad +exo, defender, teams, aad, onedrive, sharepoint +exo, defender, teams, aad, sharepoint, onedrive +exo, defender, teams, onedrive, aad, sharepoint +exo, defender, teams, onedrive, sharepoint, aad +exo, defender, teams, sharepoint, aad, onedrive +exo, defender, teams, sharepoint, onedrive, aad +exo, onedrive, aad, defender, sharepoint, teams +exo, onedrive, aad, defender, teams, sharepoint +exo, onedrive, aad, sharepoint, defender, teams +exo, onedrive, aad, sharepoint, teams, defender +exo, onedrive, aad, teams, defender, sharepoint +exo, onedrive, aad, teams, sharepoint, defender +exo, onedrive, defender, aad, sharepoint, teams +exo, onedrive, defender, aad, teams, sharepoint +exo, onedrive, defender, sharepoint, aad, teams +exo, onedrive, defender, sharepoint, teams, aad +exo, onedrive, defender, teams, aad, sharepoint +exo, onedrive, defender, teams, sharepoint, aad +exo, onedrive, sharepoint, aad, defender, teams +exo, onedrive, sharepoint, aad, teams, defender +exo, onedrive, sharepoint, defender, aad, teams +exo, onedrive, sharepoint, defender, teams, aad +exo, onedrive, sharepoint, teams, aad, defender +exo, onedrive, sharepoint, teams, defender, aad +exo, onedrive, teams, aad, defender, sharepoint +exo, onedrive, teams, aad, sharepoint, defender +exo, onedrive, teams, defender, aad, sharepoint +exo, onedrive, teams, defender, sharepoint, aad +exo, onedrive, teams, sharepoint, aad, defender +exo, onedrive, teams, sharepoint, defender, aad +exo, sharepoint, aad, defender, onedrive, teams +exo, sharepoint, aad, defender, teams, onedrive +exo, sharepoint, aad, onedrive, defender, teams +exo, sharepoint, aad, onedrive, teams, defender +exo, sharepoint, aad, teams, defender, onedrive +exo, sharepoint, aad, teams, onedrive, defender +exo, sharepoint, defender, aad, onedrive, teams +exo, sharepoint, defender, aad, teams, onedrive +exo, sharepoint, defender, onedrive, aad, teams +exo, sharepoint, defender, onedrive, teams, aad +exo, sharepoint, defender, teams, aad, onedrive +exo, sharepoint, defender, teams, onedrive, aad +exo, sharepoint, onedrive, aad, defender, teams +exo, sharepoint, onedrive, aad, teams, defender +exo, sharepoint, onedrive, defender, aad, teams +exo, sharepoint, onedrive, defender, teams, aad +exo, sharepoint, onedrive, teams, aad, defender +exo, sharepoint, onedrive, teams, defender, aad +exo, sharepoint, teams, aad, defender, onedrive +exo, sharepoint, teams, aad, onedrive, defender +exo, sharepoint, teams, defender, aad, onedrive +exo, sharepoint, teams, defender, onedrive, aad +exo, sharepoint, teams, onedrive, aad, defender +exo, sharepoint, teams, onedrive, defender, aad +exo, teams, aad, defender, onedrive, sharepoint +exo, teams, aad, defender, sharepoint, onedrive +exo, teams, aad, onedrive, defender, sharepoint +exo, teams, aad, onedrive, sharepoint, defender +exo, teams, aad, sharepoint, defender, onedrive +exo, teams, aad, sharepoint, onedrive, defender +exo, teams, defender, aad, onedrive, sharepoint +exo, teams, defender, aad, sharepoint, onedrive +exo, teams, defender, onedrive, aad, sharepoint +exo, teams, defender, onedrive, sharepoint, aad +exo, teams, defender, sharepoint, aad, onedrive +exo, teams, defender, sharepoint, onedrive, aad +exo, teams, onedrive, aad, defender, sharepoint +exo, teams, onedrive, aad, sharepoint, defender +exo, teams, onedrive, defender, aad, sharepoint +exo, teams, onedrive, defender, sharepoint, aad +exo, teams, onedrive, sharepoint, aad, defender +exo, teams, onedrive, sharepoint, defender, aad +exo, teams, sharepoint, aad, defender, onedrive +exo, teams, sharepoint, aad, onedrive, defender +exo, teams, sharepoint, defender, aad, onedrive +exo, teams, sharepoint, defender, onedrive, aad +exo, teams, sharepoint, onedrive, aad, defender +exo, teams, sharepoint, onedrive, defender, aad +onedrive, aad, defender, exo, sharepoint, teams +onedrive, aad, defender, exo, teams, sharepoint +onedrive, aad, defender, sharepoint, exo, teams +onedrive, aad, defender, sharepoint, teams, exo +onedrive, aad, defender, teams, exo, sharepoint +onedrive, aad, defender, teams, sharepoint, exo +onedrive, aad, exo, defender, sharepoint, teams +onedrive, aad, exo, defender, teams, sharepoint +onedrive, aad, exo, sharepoint, defender, teams +onedrive, aad, exo, sharepoint, teams, defender +onedrive, aad, exo, teams, defender, sharepoint +onedrive, aad, exo, teams, sharepoint, defender +onedrive, aad, sharepoint, defender, exo, teams +onedrive, aad, sharepoint, defender, teams, exo +onedrive, aad, sharepoint, exo, defender, teams +onedrive, aad, sharepoint, exo, teams, defender +onedrive, aad, sharepoint, teams, defender, exo +onedrive, aad, sharepoint, teams, exo, defender +onedrive, aad, teams, defender, exo, sharepoint +onedrive, aad, teams, defender, sharepoint, exo +onedrive, aad, teams, exo, defender, sharepoint +onedrive, aad, teams, exo, sharepoint, defender +onedrive, aad, teams, sharepoint, defender, exo +onedrive, aad, teams, sharepoint, exo, defender +onedrive, defender, aad, exo, sharepoint, teams +onedrive, defender, aad, exo, teams, sharepoint +onedrive, defender, aad, sharepoint, exo, teams +onedrive, defender, aad, sharepoint, teams, exo +onedrive, defender, aad, teams, exo, sharepoint +onedrive, defender, aad, teams, sharepoint, exo +onedrive, defender, exo, aad, sharepoint, teams +onedrive, defender, exo, aad, teams, sharepoint +onedrive, defender, exo, sharepoint, aad, teams +onedrive, defender, exo, sharepoint, teams, aad +onedrive, defender, exo, teams, aad, sharepoint +onedrive, defender, exo, teams, sharepoint, aad +onedrive, defender, sharepoint, aad, exo, teams +onedrive, defender, sharepoint, aad, teams, exo +onedrive, defender, sharepoint, exo, aad, teams +onedrive, defender, sharepoint, exo, teams, aad +onedrive, defender, sharepoint, teams, aad, exo +onedrive, defender, sharepoint, teams, exo, aad +onedrive, defender, teams, aad, exo, sharepoint +onedrive, defender, teams, aad, sharepoint, exo +onedrive, defender, teams, exo, aad, sharepoint +onedrive, defender, teams, exo, sharepoint, aad +onedrive, defender, teams, sharepoint, aad, exo +onedrive, defender, teams, sharepoint, exo, aad +onedrive, exo, aad, defender, sharepoint, teams +onedrive, exo, aad, defender, teams, sharepoint +onedrive, exo, aad, sharepoint, defender, teams +onedrive, exo, aad, sharepoint, teams, defender +onedrive, exo, aad, teams, defender, sharepoint +onedrive, exo, aad, teams, sharepoint, defender +onedrive, exo, defender, aad, sharepoint, teams +onedrive, exo, defender, aad, teams, sharepoint +onedrive, exo, defender, sharepoint, aad, teams +onedrive, exo, defender, sharepoint, teams, aad +onedrive, exo, defender, teams, aad, sharepoint +onedrive, exo, defender, teams, sharepoint, aad +onedrive, exo, sharepoint, aad, defender, teams +onedrive, exo, sharepoint, aad, teams, defender +onedrive, exo, sharepoint, defender, aad, teams +onedrive, exo, sharepoint, defender, teams, aad +onedrive, exo, sharepoint, teams, aad, defender +onedrive, exo, sharepoint, teams, defender, aad +onedrive, exo, teams, aad, defender, sharepoint +onedrive, exo, teams, aad, sharepoint, defender +onedrive, exo, teams, defender, aad, sharepoint +onedrive, exo, teams, defender, sharepoint, aad +onedrive, exo, teams, sharepoint, aad, defender +onedrive, exo, teams, sharepoint, defender, aad +onedrive, sharepoint, aad, defender, exo, teams +onedrive, sharepoint, aad, defender, teams, exo +onedrive, sharepoint, aad, exo, defender, teams +onedrive, sharepoint, aad, exo, teams, defender +onedrive, sharepoint, aad, teams, defender, exo +onedrive, sharepoint, aad, teams, exo, defender +onedrive, sharepoint, defender, aad, exo, teams +onedrive, sharepoint, defender, aad, teams, exo +onedrive, sharepoint, defender, exo, aad, teams +onedrive, sharepoint, defender, exo, teams, aad +onedrive, sharepoint, defender, teams, aad, exo +onedrive, sharepoint, defender, teams, exo, aad +onedrive, sharepoint, exo, aad, defender, teams +onedrive, sharepoint, exo, aad, teams, defender +onedrive, sharepoint, exo, defender, aad, teams +onedrive, sharepoint, exo, defender, teams, aad +onedrive, sharepoint, exo, teams, aad, defender +onedrive, sharepoint, exo, teams, defender, aad +onedrive, sharepoint, teams, aad, defender, exo +onedrive, sharepoint, teams, aad, exo, defender +onedrive, sharepoint, teams, defender, aad, exo +onedrive, sharepoint, teams, defender, exo, aad +onedrive, sharepoint, teams, exo, aad, defender +onedrive, sharepoint, teams, exo, defender, aad +onedrive, teams, aad, defender, exo, sharepoint +onedrive, teams, aad, defender, sharepoint, exo +onedrive, teams, aad, exo, defender, sharepoint +onedrive, teams, aad, exo, sharepoint, defender +onedrive, teams, aad, sharepoint, defender, exo +onedrive, teams, aad, sharepoint, exo, defender +onedrive, teams, defender, aad, exo, sharepoint +onedrive, teams, defender, aad, sharepoint, exo +onedrive, teams, defender, exo, aad, sharepoint +onedrive, teams, defender, exo, sharepoint, aad +onedrive, teams, defender, sharepoint, aad, exo +onedrive, teams, defender, sharepoint, exo, aad +onedrive, teams, exo, aad, defender, sharepoint +onedrive, teams, exo, aad, sharepoint, defender +onedrive, teams, exo, defender, aad, sharepoint +onedrive, teams, exo, defender, sharepoint, aad +onedrive, teams, exo, sharepoint, aad, defender +onedrive, teams, exo, sharepoint, defender, aad +onedrive, teams, sharepoint, aad, defender, exo +onedrive, teams, sharepoint, aad, exo, defender +onedrive, teams, sharepoint, defender, aad, exo +onedrive, teams, sharepoint, defender, exo, aad +onedrive, teams, sharepoint, exo, aad, defender +onedrive, teams, sharepoint, exo, defender, aad +sharepoint, aad, defender, exo, onedrive, teams +sharepoint, aad, defender, exo, teams, onedrive +sharepoint, aad, defender, onedrive, exo, teams +sharepoint, aad, defender, onedrive, teams, exo +sharepoint, aad, defender, teams, exo, onedrive +sharepoint, aad, defender, teams, onedrive, exo +sharepoint, aad, exo, defender, onedrive, teams +sharepoint, aad, exo, defender, teams, onedrive +sharepoint, aad, exo, onedrive, defender, teams +sharepoint, aad, exo, onedrive, teams, defender +sharepoint, aad, exo, teams, defender, onedrive +sharepoint, aad, exo, teams, onedrive, defender +sharepoint, aad, onedrive, defender, exo, teams +sharepoint, aad, onedrive, defender, teams, exo +sharepoint, aad, onedrive, exo, defender, teams +sharepoint, aad, onedrive, exo, teams, defender +sharepoint, aad, onedrive, teams, defender, exo +sharepoint, aad, onedrive, teams, exo, defender +sharepoint, aad, teams, defender, exo, onedrive +sharepoint, aad, teams, defender, onedrive, exo +sharepoint, aad, teams, exo, defender, onedrive +sharepoint, aad, teams, exo, onedrive, defender +sharepoint, aad, teams, onedrive, defender, exo +sharepoint, aad, teams, onedrive, exo, defender +sharepoint, defender, aad, exo, onedrive, teams +sharepoint, defender, aad, exo, teams, onedrive +sharepoint, defender, aad, onedrive, exo, teams +sharepoint, defender, aad, onedrive, teams, exo +sharepoint, defender, aad, teams, exo, onedrive +sharepoint, defender, aad, teams, onedrive, exo +sharepoint, defender, exo, aad, onedrive, teams +sharepoint, defender, exo, aad, teams, onedrive +sharepoint, defender, exo, onedrive, aad, teams +sharepoint, defender, exo, onedrive, teams, aad +sharepoint, defender, exo, teams, aad, onedrive +sharepoint, defender, exo, teams, onedrive, aad +sharepoint, defender, onedrive, aad, exo, teams +sharepoint, defender, onedrive, aad, teams, exo +sharepoint, defender, onedrive, exo, aad, teams +sharepoint, defender, onedrive, exo, teams, aad +sharepoint, defender, onedrive, teams, aad, exo +sharepoint, defender, onedrive, teams, exo, aad +sharepoint, defender, teams, aad, exo, onedrive +sharepoint, defender, teams, aad, onedrive, exo +sharepoint, defender, teams, exo, aad, onedrive +sharepoint, defender, teams, exo, onedrive, aad +sharepoint, defender, teams, onedrive, aad, exo +sharepoint, defender, teams, onedrive, exo, aad +sharepoint, exo, aad, defender, onedrive, teams +sharepoint, exo, aad, defender, teams, onedrive +sharepoint, exo, aad, onedrive, defender, teams +sharepoint, exo, aad, onedrive, teams, defender +sharepoint, exo, aad, teams, defender, onedrive +sharepoint, exo, aad, teams, onedrive, defender +sharepoint, exo, defender, aad, onedrive, teams +sharepoint, exo, defender, aad, teams, onedrive +sharepoint, exo, defender, onedrive, aad, teams +sharepoint, exo, defender, onedrive, teams, aad +sharepoint, exo, defender, teams, aad, onedrive +sharepoint, exo, defender, teams, onedrive, aad +sharepoint, exo, onedrive, aad, defender, teams +sharepoint, exo, onedrive, aad, teams, defender +sharepoint, exo, onedrive, defender, aad, teams +sharepoint, exo, onedrive, defender, teams, aad +sharepoint, exo, onedrive, teams, aad, defender +sharepoint, exo, onedrive, teams, defender, aad +sharepoint, exo, teams, aad, defender, onedrive +sharepoint, exo, teams, aad, onedrive, defender +sharepoint, exo, teams, defender, aad, onedrive +sharepoint, exo, teams, defender, onedrive, aad +sharepoint, exo, teams, onedrive, aad, defender +sharepoint, exo, teams, onedrive, defender, aad +sharepoint, onedrive, aad, defender, exo, teams +sharepoint, onedrive, aad, defender, teams, exo +sharepoint, onedrive, aad, exo, defender, teams +sharepoint, onedrive, aad, exo, teams, defender +sharepoint, onedrive, aad, teams, defender, exo +sharepoint, onedrive, aad, teams, exo, defender +sharepoint, onedrive, defender, aad, exo, teams +sharepoint, onedrive, defender, aad, teams, exo +sharepoint, onedrive, defender, exo, aad, teams +sharepoint, onedrive, defender, exo, teams, aad +sharepoint, onedrive, defender, teams, aad, exo +sharepoint, onedrive, defender, teams, exo, aad +sharepoint, onedrive, exo, aad, defender, teams +sharepoint, onedrive, exo, aad, teams, defender +sharepoint, onedrive, exo, defender, aad, teams +sharepoint, onedrive, exo, defender, teams, aad +sharepoint, onedrive, exo, teams, aad, defender +sharepoint, onedrive, exo, teams, defender, aad +sharepoint, onedrive, teams, aad, defender, exo +sharepoint, onedrive, teams, aad, exo, defender +sharepoint, onedrive, teams, defender, aad, exo +sharepoint, onedrive, teams, defender, exo, aad +sharepoint, onedrive, teams, exo, aad, defender +sharepoint, onedrive, teams, exo, defender, aad +sharepoint, teams, aad, defender, exo, onedrive +sharepoint, teams, aad, defender, onedrive, exo +sharepoint, teams, aad, exo, defender, onedrive +sharepoint, teams, aad, exo, onedrive, defender +sharepoint, teams, aad, onedrive, defender, exo +sharepoint, teams, aad, onedrive, exo, defender +sharepoint, teams, defender, aad, exo, onedrive +sharepoint, teams, defender, aad, onedrive, exo +sharepoint, teams, defender, exo, aad, onedrive +sharepoint, teams, defender, exo, onedrive, aad +sharepoint, teams, defender, onedrive, aad, exo +sharepoint, teams, defender, onedrive, exo, aad +sharepoint, teams, exo, aad, defender, onedrive +sharepoint, teams, exo, aad, onedrive, defender +sharepoint, teams, exo, defender, aad, onedrive +sharepoint, teams, exo, defender, onedrive, aad +sharepoint, teams, exo, onedrive, aad, defender +sharepoint, teams, exo, onedrive, defender, aad +sharepoint, teams, onedrive, aad, defender, exo +sharepoint, teams, onedrive, aad, exo, defender +sharepoint, teams, onedrive, defender, aad, exo +sharepoint, teams, onedrive, defender, exo, aad +sharepoint, teams, onedrive, exo, aad, defender +sharepoint, teams, onedrive, exo, defender, aad +teams, aad, defender, exo, onedrive, sharepoint +teams, aad, defender, exo, sharepoint, onedrive +teams, aad, defender, onedrive, exo, sharepoint +teams, aad, defender, onedrive, sharepoint, exo +teams, aad, defender, sharepoint, exo, onedrive +teams, aad, defender, sharepoint, onedrive, exo +teams, aad, exo, defender, onedrive, sharepoint +teams, aad, exo, defender, sharepoint, onedrive +teams, aad, exo, onedrive, defender, sharepoint +teams, aad, exo, onedrive, sharepoint, defender +teams, aad, exo, sharepoint, defender, onedrive +teams, aad, exo, sharepoint, onedrive, defender +teams, aad, onedrive, defender, exo, sharepoint +teams, aad, onedrive, defender, sharepoint, exo +teams, aad, onedrive, exo, defender, sharepoint +teams, aad, onedrive, exo, sharepoint, defender +teams, aad, onedrive, sharepoint, defender, exo +teams, aad, onedrive, sharepoint, exo, defender +teams, aad, sharepoint, defender, exo, onedrive +teams, aad, sharepoint, defender, onedrive, exo +teams, aad, sharepoint, exo, defender, onedrive +teams, aad, sharepoint, exo, onedrive, defender +teams, aad, sharepoint, onedrive, defender, exo +teams, aad, sharepoint, onedrive, exo, defender +teams, defender, aad, exo, onedrive, sharepoint +teams, defender, aad, exo, sharepoint, onedrive +teams, defender, aad, onedrive, exo, sharepoint +teams, defender, aad, onedrive, sharepoint, exo +teams, defender, aad, sharepoint, exo, onedrive +teams, defender, aad, sharepoint, onedrive, exo +teams, defender, exo, aad, onedrive, sharepoint +teams, defender, exo, aad, sharepoint, onedrive +teams, defender, exo, onedrive, aad, sharepoint +teams, defender, exo, onedrive, sharepoint, aad +teams, defender, exo, sharepoint, aad, onedrive +teams, defender, exo, sharepoint, onedrive, aad +teams, defender, onedrive, aad, exo, sharepoint +teams, defender, onedrive, aad, sharepoint, exo +teams, defender, onedrive, exo, aad, sharepoint +teams, defender, onedrive, exo, sharepoint, aad +teams, defender, onedrive, sharepoint, aad, exo +teams, defender, onedrive, sharepoint, exo, aad +teams, defender, sharepoint, aad, exo, onedrive +teams, defender, sharepoint, aad, onedrive, exo +teams, defender, sharepoint, exo, aad, onedrive +teams, defender, sharepoint, exo, onedrive, aad +teams, defender, sharepoint, onedrive, aad, exo +teams, defender, sharepoint, onedrive, exo, aad +teams, exo, aad, defender, onedrive, sharepoint +teams, exo, aad, defender, sharepoint, onedrive +teams, exo, aad, onedrive, defender, sharepoint +teams, exo, aad, onedrive, sharepoint, defender +teams, exo, aad, sharepoint, defender, onedrive +teams, exo, aad, sharepoint, onedrive, defender +teams, exo, defender, aad, onedrive, sharepoint +teams, exo, defender, aad, sharepoint, onedrive +teams, exo, defender, onedrive, aad, sharepoint +teams, exo, defender, onedrive, sharepoint, aad +teams, exo, defender, sharepoint, aad, onedrive +teams, exo, defender, sharepoint, onedrive, aad +teams, exo, onedrive, aad, defender, sharepoint +teams, exo, onedrive, aad, sharepoint, defender +teams, exo, onedrive, defender, aad, sharepoint +teams, exo, onedrive, defender, sharepoint, aad +teams, exo, onedrive, sharepoint, aad, defender +teams, exo, onedrive, sharepoint, defender, aad +teams, exo, sharepoint, aad, defender, onedrive +teams, exo, sharepoint, aad, onedrive, defender +teams, exo, sharepoint, defender, aad, onedrive +teams, exo, sharepoint, defender, onedrive, aad +teams, exo, sharepoint, onedrive, aad, defender +teams, exo, sharepoint, onedrive, defender, aad +teams, onedrive, aad, defender, exo, sharepoint +teams, onedrive, aad, defender, sharepoint, exo +teams, onedrive, aad, exo, defender, sharepoint +teams, onedrive, aad, exo, sharepoint, defender +teams, onedrive, aad, sharepoint, defender, exo +teams, onedrive, aad, sharepoint, exo, defender +teams, onedrive, defender, aad, exo, sharepoint +teams, onedrive, defender, aad, sharepoint, exo +teams, onedrive, defender, exo, aad, sharepoint +teams, onedrive, defender, exo, sharepoint, aad +teams, onedrive, defender, sharepoint, aad, exo +teams, onedrive, defender, sharepoint, exo, aad +teams, onedrive, exo, aad, defender, sharepoint +teams, onedrive, exo, aad, sharepoint, defender +teams, onedrive, exo, defender, aad, sharepoint +teams, onedrive, exo, defender, sharepoint, aad +teams, onedrive, exo, sharepoint, aad, defender +teams, onedrive, exo, sharepoint, defender, aad +teams, onedrive, sharepoint, aad, defender, exo +teams, onedrive, sharepoint, aad, exo, defender +teams, onedrive, sharepoint, defender, aad, exo +teams, onedrive, sharepoint, defender, exo, aad +teams, onedrive, sharepoint, exo, aad, defender +teams, onedrive, sharepoint, exo, defender, aad +teams, sharepoint, aad, defender, exo, onedrive +teams, sharepoint, aad, defender, onedrive, exo +teams, sharepoint, aad, exo, defender, onedrive +teams, sharepoint, aad, exo, onedrive, defender +teams, sharepoint, aad, onedrive, defender, exo +teams, sharepoint, aad, onedrive, exo, defender +teams, sharepoint, defender, aad, exo, onedrive +teams, sharepoint, defender, aad, onedrive, exo +teams, sharepoint, defender, exo, aad, onedrive +teams, sharepoint, defender, exo, onedrive, aad +teams, sharepoint, defender, onedrive, aad, exo +teams, sharepoint, defender, onedrive, exo, aad +teams, sharepoint, exo, aad, defender, onedrive +teams, sharepoint, exo, aad, onedrive, defender +teams, sharepoint, exo, defender, aad, onedrive +teams, sharepoint, exo, defender, onedrive, aad +teams, sharepoint, exo, onedrive, aad, defender +teams, sharepoint, exo, onedrive, defender, aad +teams, sharepoint, onedrive, aad, defender, exo +teams, sharepoint, onedrive, aad, exo, defender +teams, sharepoint, onedrive, defender, aad, exo +teams, sharepoint, onedrive, defender, exo, aad +teams, sharepoint, onedrive, exo, aad, defender +teams, sharepoint, onedrive, exo, defender, aad \ No newline at end of file diff --git a/Testing/Functional/Auto/MinimumTest.txt b/Testing/Functional/Auto/MinimumTest.txt new file mode 100644 index 0000000000..82dc04343f --- /dev/null +++ b/Testing/Functional/Auto/MinimumTest.txt @@ -0,0 +1,41 @@ +aad +defender +exo +onedrive +sharepoint +teams +aad, defender +aad, exo +aad, onedrive +aad, sharepoint +aad, teams +defender, exo +defender, onedrive +defender, sharepoint +defender, teams +exo, onedrive +exo, sharepoint +exo, teams +onedrive, sharepoint +onedrive, teams +sharepoint, teams +aad, defender, exo +aad, defender, onedrive +aad, defender, sharepoint +aad, defender, teams +defender, exo, onedrive +defender, exo, sharepoint +defender, exo, teams +exo, onedrive, sharepoint +exo, onedrive, teams +onedrive, sharepoint, teams +aad, defender, exo, onedrive +aad, defender, exo, sharepoint +aad, defender, exo, teams +defender, exo, onedrive, sharepoint +defender, exo, onedrive, teams +exo, onedrive, sharepoint, teams +aad, defender, exo, onedrive, sharepoint +aad, defender, exo, onedrive, teams +defender, exo, onedrive, sharepoint, teams +aad, defender, exo, onedrive, sharepoint, teams \ No newline at end of file diff --git a/Testing/Functional/Auto/SimpleTest.txt b/Testing/Functional/Auto/SimpleTest.txt new file mode 100644 index 0000000000..f32126ea51 --- /dev/null +++ b/Testing/Functional/Auto/SimpleTest.txt @@ -0,0 +1,6 @@ +aad +defender +exo +onedrive +sharepoint +teams \ No newline at end of file diff --git a/Testing/Functional/RegoCachedProviderTesting.ps1 b/Testing/Functional/RegoCachedProviderTesting.ps1 new file mode 100644 index 0000000000..77b5c1c2f8 --- /dev/null +++ b/Testing/Functional/RegoCachedProviderTesting.ps1 @@ -0,0 +1,64 @@ +# +# For Rego testing with a static provider JSON. +# When pure Rego testing it makes sense to export the provider only once. +# +# DO NOT confuse this script with the Rego Unit tests script + +# +# The tenant name in the report will display Rego Testing which IS intentional. +# This is so that this test script can be run on any cached provider JSON +# + +# Set $true for the first run of this script +# then set this to be $false each subsequent run +param ( + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", "onedrive", '*', IgnoreCase = $false)] + [string[]] + $ProductNames = '*', # The specific products that you want the tool to assess. + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string] + $OutPath = ".\Testing\Functional\Reports", # output directory + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [ValidateSet($true, $false)] + [boolean] + $LogIn = $false, # Set $true to authenticate yourself to a tenant or if you are already authenticated set to $false to avoid reauthentication + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [ValidateSet($true, $false)] + [boolean] + $ExportProvider = $true, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [ValidateSet($true, $false)] + [boolean] + $Quiet = $True # Supress report poping up after run +) + +$M365Environment = "gcc" +$OPAPath = "./" # Path to OPA Executable + +$RunCachedParams = @{ + 'ExportProvider' = $ExportProvider; + 'Login' = $Login; + 'ProductNames' = $ProductNames; + 'M365Environment' = $M365Environment; + 'OPAPath' = $OPAPath; + 'OutPath' = $OutPath; + 'Quiet' = $Quiet; +} + +$Root = @((Get-ChildItem -Recurse -Filter CISA-SCuBA-M365-SCB -Directory -ErrorAction SilentlyContinue -Path $Home).FullName) +Set-Location $Root[0] +$ManifestPath = Join-Path -Path "./PowerShell" -ChildPath "ScubaGear" +Remove-Module "ScubaGear" -ErrorAction "SilentlyContinue" # For dev work +####### +Import-Module $ManifestPath -ErrorAction Stop +Invoke-RunCached @RunCachedParams \ No newline at end of file diff --git a/Testing/RunFunctionalTests.ps1 b/Testing/RunFunctionalTests.ps1 new file mode 100644 index 0000000000..51b3a694a8 --- /dev/null +++ b/Testing/RunFunctionalTests.ps1 @@ -0,0 +1,471 @@ +<# + .SYNOPSIS + Test SCuBA tool against various outputs for functional testing. + + .DESCRIPTION + This script executes prexisting provider exports against the Rego code and compares output against saved runs for + regression testing. + + .OUTPUTS + Text output that indicates how many tests were consistent or different from the saved test results. + + .EXAMPLE + .\RunFunctionalTests.ps1 + Running against all Rego regression tests is default, no flags necessary. + + .EXAMPLE + .\RunFunctionalTests.ps1 -p teams,exo,defender,aad + Runs all test cases for specified products. Products must be specified with -p parameter. + Valid product names are: aad, defender, exo, onedrive, powerplatform, sharepoint, teams, and '*'. + Runs all products on default. + + .EXAMPLE + .\RunFunctionalTests.ps1 -t Rego -p * + To run a specific type of test, must indicate test with -t. Possible types are: Rego, Full + Runs Rego regression test on default. + + .EXAMPLE + .\RunFunctionalTests.ps1 -a Simple + To run a predefined set of tests, must indicate type with -a. Possible types are: Simple, Minimum, Extreme + CAUTION when using Extreme, there are 1957 test cases. Can be used when running against tenant or Rego regression test + + .EXAMPLE + .\RunFunctionalTests.ps1 -o .\Functional\Reports + Enter the file path for the SCuBA working directory. This is where the ProviderExport, TestResults, and Report will be generated by the tool. + The default path is .\Functional\Reports. + + .EXAMPLE + .\RunFunctionalTests.ps1 -s .\Functional\Archive + Enter the file path for where the test results from the Rego regression test will be saved. The default path is .\Functional\Archive. + + .EXAMPLE + .\RunFunctionalTests.ps1 -i .\BasicRegressionTests + Enter the directory path where the saved provider exports & test results are for the rego test. The default path is .\Functional\BasicRegressionTests + + .EXAMPLE + .\RunFunctionalTests.ps1 -v + Outputs the verbose results for the test. + + .EXAMPLE + .\RunFunctionalTests.ps1 -q $false + Choose to supress the reports from open immediately after generation. + #> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'VerboseOutput', + Justification = 'variable is used in another scope')] + +[CmdletBinding()] +param ( + <# + .PARAMETER Products + Takes a comma seperated list of product names to run the script + against: 'teams', 'exo', 'defender', 'aad', 'powerplatform', 'sharepoint', 'onedrive', '*'. Runs all on default. + #> + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [ValidateSet('teams', 'exo', 'defender', 'aad', 'powerplatform', 'sharepoint', 'onedrive', '*', IgnoreCase = $false)] + [Alias('p')] + [string[]]$Products = '*', + + <# + .PARAMETER TestType + Takes the user's selection of test type: Rego, Full. Runs Rego on default. + #> + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [ValidateSet('Rego', 'Full')] + [Alias('t')] + [string]$TestType = 'Rego', + + <# + .PARAMETER Auto + Takes the user's selection of auto test type: Simple, Minimum, Extreme. + #> + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [ValidateSet('Simple', 'Minimum', 'Extreme')] + [Alias('a')] + [string]$Auto = '', + + <# + .PARAMETER Out + Takes the user's selection of SCuBA's working directory. + #> + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [Alias('o')] + [string]$Out = '.\Functional\Reports', + + <# + .PARAMETER Save + Takes the user's selection of where test results from regression test is to be saved. + #> + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [Alias('s')] + [string]$Save = '.\Functional\Archive', + + <# + .PARAMETER RegressionTests + Takes the directory path to the Regression Tests. + #> + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [Alias('i')] + [string]$RegressionTests = (Join-Path -Path $Home -ChildPath 'BasicRegressionTests'), + + <# + .PARAMETER VerboseOutput + Prints the verbose output. + #> + [Parameter(Mandatory = $false)] + [Alias('v')] + [switch]$VerboseOutput, + + <# + .PARAMETER Quiet + Runs SCuBA in silent mode so the reports do not open immediately after generation. + #> + [Parameter(Mandatory = $false)] + [Alias('q')] + [switch]$Quiet +) + +function Compare-Results { + param ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$Filename + ) + + $ResultRegression = $Filename -replace 'ProviderExport', 'TestResults' + $RegressionJson = Get-Content $ResultRegression | ConvertFrom-Json + $TestResultFile = Get-ChildItem $Out -Filter *.json | Where-Object { $_.Name -match 'TestResults' } | Select-Object Fullname + $ResultNew = Get-SavedFilename $ResultRegression + Copy-Item -Path $TestResultFile.Fullname -Destination $ResultNew + + if (Confirm-FileExists $ResultNew) { + $NewJson = Get-Content $ResultNew | ConvertFrom-Json + + if (($RegressionJson | ConvertTo-Json -Compress) -eq ($NewJson | ConvertTo-Json -Compress)) { + return "`n`t$(Split-Path -Path $ResultRegression -Leaf -Resolve) : CONSISTENT" + } + else { + try { + code --diff $ResultRegression $ResultNew + } + catch { + Compare-Object (($RegressionJson | ConvertTo-Json) -split '\r?\n') (($NewJson | ConvertTo-Json) -split '\r?\n') + Write-Output "`n==== $(Split-Path -Path $ResultRegression -Leaf -Resolve) vs $(Split-Path -Path $ResultNew -Leaf -Resolve) ====`n" | Out-Host + } + } + + return "`n`t$(Split-Path -Path $ResultRegression -Leaf -Resolve) : DIFFERENT" + } +} + +function Confirm-FileExists { + param ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$Filename + ) + + if (Test-Path -Path $Filename -PathType Leaf) { + return $true + } + else { + Write-Warning "$Filename not found`nSkipping......`n" | Out-Host + } + return $false +} + +function Get-SavedFilename { + param ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$Filepath + ) + + $Filename = Split-Path -Path $Filepath -Leaf -Resolve + $Date = Get-Date -Format 'MMddyyyy' + $NewFilename = $Filename -replace '[0-9]+\.json', ($Date + '.json') + + return Join-Path -Path (Get-Item $Save) -ChildPath $NewFilename +} + +function Get-ProviderExportFiles { + param ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$FilePath + ) + + try { + $TestFiles = (Get-ChildItem $FilePath -ErrorAction Stop | Where-Object { $_.Name -match 'ProviderExport' } | Select-Object FullName).FullName + return $true, $TestFiles + } + catch { + Write-Warning "$Product is missing, no files for Rego test found`nSkipping......`n" | Out-Host + } + + return $false +} + +function Write-RegoOutput { + param ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [ValidateSet('aad', 'defender', 'exo', 'onedrive', 'powerplatform', 'sharepoint', 'teams', '*', IgnoreCase = $false)] + [string[]]$Products, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string[]]$RegoResults + ) + + if ($VerboseOutput.IsPresent) { + Write-Output "`n`t=== Testing @($($Products -join ",")) ===$($RegoResults[2])" + } + elseif ($Result[3] -ne "") { + Write-Output "`n`t=== Testing @($($Products -join ",")) ===$($RegoResults[3])" + } +} + +function Read-AutoFile { + param ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$Filename + ) + + $Result = @(0, 0) + $LogIn = $true + if ($TestType -eq 'Full') { + if ($Quiet.IsPresent -eq $false) { + $Quiet = Confirm-UserSelection 'Do you want reports to open immediately after generation [y/n]' + } + } + + if (Confirm-FileExists $Filename) { + foreach ($Products in Get-Content $Filename) { + if ($TestType -eq 'Full') { + Invoke-Full $Products -LogIn $LogIn -Silent $Quiet + $LogIn = $false + } + elseif ($TestType -eq 'Rego') { + $Result = Invoke-Rego -Products $Products -PassCount $Result[0] -TotalCount $Result[1] + } + } + if (($TestType -eq 'Rego') -and ($Result[1] -gt 0)) { + Write-RegoOutput $Products $Result + Write-Output "`n`tCONSISTENT $($Result[0])/$($Result[1])`n" + } + } +} + +function Invoke-Rego { + param ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [ValidateSet('aad', 'defender', 'exo', 'onedrive', 'powerplatform', 'sharepoint', 'teams', '*', IgnoreCase = $false)] + [string[]]$Products, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [int]$PassCount, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [int]$TotalCount + ) + + $ExportFilename = Join-Path -Path $Out -ChildPath 'ProviderSettingsExport.json' + $VerboseOutput = ' ' + $FailString = ' ' + + foreach ($Product in $Products) { + $FilePath = Join-Path -Path $RegressionTests -ChildPath $Product + $FilesFound = Get-ProviderExportFiles $FilePath + + if ($FilesFound[0]) { + $TotalCount += $FilesFound[1].Length + + foreach ($File in $FilesFound[1]) { + + if (Confirm-FileExists $File) { + Copy-Item -Path $File -Destination $ExportFilename + + if (Confirm-FileExists $ExportFilename) { + try { + .\Functional\RegoCachedProviderTesting.ps1 -ProductNames $Product -ExportProvider $false -OutPath $Out + } + catch { + Set-Location $PSScriptRoot + Write-Error "Unknown problem running '.\Functional\RegoCachedProviderTesting.ps1', please report." + exit + } + Set-Location $PSScriptRoot + $ResultString = Compare-Results $File + + if ($ResultString.Contains('CONSISTENT')) { + $PassCount += 1 + } + else { + $FailString += $ResultString + } + + $VerboseOutput += $ResultString + Remove-Item $ExportFilename + } + } + } + } + } + + return $PassCount, $TotalCount, $VerboseOutput, $FailString +} + +function Invoke-Full { + param ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [ValidateSet('aad', 'defender', 'exo', 'onedrive', 'powerplatform', 'sharepoint', 'teams', '*', IgnoreCase = $false)] + [string[]]$Products, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [ValidateSet($true, $false)] + [boolean] + $LogIn = $false, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [ValidateSet($true, $false)] + [boolean] + $Quiet = $True + ) + try { + .\Functional\RegoCachedProviderTesting.ps1 -ProductNames $Products -OutPath $Out -LogIn $LogIn -Quiet $Quiet + } + catch { + Set-Location $PSScriptRoot + Write-Error "Unknown problem running '.\Functional\RegoCachedProviderTesting.ps1', please report." + exit + } + Set-Location $PSScriptRoot + +} + +function Invoke-Auto { + param ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [ValidateSet('Simple', 'Minimum', 'Extreme')] + [string]$Auto + ) + + $Filename = '' + + switch ($Auto) { + 'Extreme' { + Write-Warning "File has 1957 tests!`n" | Out-Host + if ((Confirm-UserSelection "Do you wish to continue [y/n]?") -eq $false) { + Write-Output "Canceling....." + exit + } + Write-Output "Continuing.....`nEnter Ctrl+C to cancel`n" + + $Filename = "Functional\Auto\ExtremeTest.txt" + } + 'Minimum' { + $Filename = "Functional\Auto\MinimumTest.txt" + } + 'Simple' { + $Filename = "Functional\Auto\SimpleTest.txt" + } + Default { + Write-Error "Uknown auto test '$Auto'" + } + } + + Read-AutoFile $Filename +} + +function Confirm-Continue { + param ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [String]$Prompt + ) + + $Choice = Read-Host -Prompt $Prompt + + if (($Choice -ne 'y') -or ($Choice -ne 'yes')) { + return $true + } + + return $false +} + +function New-Folders { + param ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [String]$Folder + ) + + if ((Test-Path $Folder) -eq $false) { + New-Item $Folder -ItemType Directory + } +} + +function Get-AbsolutePath { + param ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$FilePath + ) + + $NewFilePath = (Get-ChildItem -Recurse -Filter $(Split-Path -Path $FilePath -Leaf) -Directory -ErrorAction SilentlyContinue -Path $(Split-Path -Path $FilePath)).FullName + + if ($null -eq $NewFilePath) { + Write-Error "$FilePath NOT FOUND" | Out-Host + exit + } + return $NewFilePath +} + +New-Folders $Out +$Out = Get-AbsolutePath $Out + +if ($Products[0] -eq '*') { + [string[]] $Products = ((Get-ChildItem -Path 'Unit\Rego' -Recurse -Directory -Force -ErrorAction SilentlyContinue | + Select-Object Name).Name).toLower() +} + +if ($Auto -ne '') { + if ($TestType -eq 'Full') { + Write-Output "COMING SOON: Disabled until defender bug is fixed" + exit + } + Invoke-Auto $Auto +} + +elseif ($TestType -eq 'Rego') { + New-Folders $Save + $Save = Get-AbsolutePath $Save + $RegressionTests = Get-AbsolutePath $RegressionTests + $Result = Invoke-Rego -Products $Products -PassCount 0 -TotalCount 0 + + if ($Result[1] -gt 0) { + Write-RegoOutput $Products $Result + Write-Output "`n`tCONSISTENT $($Result[0])/$($Result[1])`n" + } +} + +else { + Write-Output "COMING SOON: Disabled until defender bug is fixed" + exit + Invoke-Full -Products Products -LogIn $true -Silent $Quiet +} \ No newline at end of file diff --git a/Testing/RunUnitTests.ps1 b/Testing/RunUnitTests.ps1 new file mode 100644 index 0000000000..970c5c8a21 --- /dev/null +++ b/Testing/RunUnitTests.ps1 @@ -0,0 +1,218 @@ +[CmdletBinding()] +param ( + [Parameter()] + [ValidateSet('AAD','Defender','EXO','OneDrive','PowerPlatform','Sharepoint','Teams')] + [string[]]$p = "", + [Parameter()] + [string[]]$b = "", + [Parameter()] + [string[]]$t = "", + [Parameter()] + [switch]$h, + [Parameter()] + [switch]$v +) + +$ScriptName = $MyInvocation.MyCommand +$FilePath = ".\Unit\Rego" + +function Show-Menu { + Write-Output "`n`t==================================== Flags ====================================" + Write-Output "`n`t-h`tshows help menu" + Write-Output "`n`t-p`tproduct name, can take a comma-separated list of product names" + Write-Output "`n`t-b`tbaseline item number, can take a comma-separated list of item numbers" + Write-Output "`n`t-t`ttest name, can take a comma-separated list of test names" + Write-Output "`n`t-v`tverbose, verbose opa output" + Write-Output "`n`t==================================== Usage ====================================" + Write-Output "`n`tRuning all tests is default, no flags are necessary" + Write-Output "`t.\$ScriptName" + Write-Output "`n`tTo run all test cases for specified products, must indicate products with -p" + Write-Output "`t.\$ScriptName [-p] " + Write-Output "`n`tTo run all test cases in baseline item numbers, must indicate product with -p" + Write-Output "`tand baseline item numbers with -b" + Write-Output "`t.\$ScriptName [-p] [-b] " + Write-Output "`n`tTo run test case for specified baseline item number must indicate product with -p," + Write-Output "`tbaseline item numberwith -b, and test cases with -t" + Write-Output "`t.\$ScriptName [-p] [-b] [-t] " + Write-Output "`n`tVerbose flag can be added to any test at beginning or end of command line" + Write-Output "`t.\$ScriptName [-v]" + Write-Output "`n`t==================================== Examples ====================================" + Write-Output "`n`t.\$ScriptName -p AAD, Defender, OneDrive" + Write-Output "`n`t.\$ScriptName -p AAD -b 01, 2, 10" + Write-Output "`n`t.\$ScriptName -p AAD -b 01 -t test_IncludeApplications_Incorrect, test_Conditions_Correct" + Write-Output "`n`t.\$ScriptName -p AAD -v" + Write-Output "`n`t.\$ScriptName -v -p AAD -b 01 -t test_IncludeApplications_Incorrect`n" + exit +} + +function Get-ErrorMsg { + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [string[]]$Flag + ) + + $FontColor = $host.ui.RawUI.ForegroundColor + $BackgroundColor = $host.ui.RawUI.BackgroundColor + $host.ui.RawUI.ForegroundColor = "Red" + $host.ui.RawUI.BackgroundColor = "Black" + switch ($Flag[0]) { + TestNameFlagsMissing { + Write-Output "ERROR: Missing value(s) to run opa for specific test case(s)" + Write-Output ".\$ScriptName [-p] [-b] [-t] `n" + } + BaselineItemFlagMissing { + Write-Output "ERROR: Missing value(s) to run opa for specific baseline item(s)" + Write-Output ".\$ScriptName [-p] [-b] `n" + } + BaselineItemNumber { + Write-Output "ERROR: Unrecognized number '$b'" + Write-Output "Must be an integer (1, 2, 3, ...) or baseline syntax (01, 02, 03..09, 10, ...)`n" + } + FileIOError { + Write-Output "ERROR: '$($Flag[1])' not found`n" + } + Default { + Write-Output "ERROR: Unknown`n" + } + } + $host.ui.RawUI.ForegroundColor = $FontColor + $host.ui.RawUI.BackgroundColor = $BackgroundColor + exit +} + +function Invoke-Product { + [CmdletBinding()] + param ( + [Parameter()] + [string]$Flag + ) + + foreach($Product in $p) { + Write-Output "`n==== Testing $Product ====" + $Directory = Join-Path -Path $FilePath -ChildPath $Product + ..\opa_windows_amd64.exe test ..\Rego\ $Directory $Flag + } + Write-Output "" +} + +function Get-Baseline { + [CmdletBinding()] + param ( + [string] $Baseline + ) + + $Tens = @('01','02','03','04','05','06','07','08','09') + if(($Baseline -match "^\d+$") -or ($Baseline -in $Tens)) { + if ([int]$Baseline -lt 10) { + $Baseline = $Tens[[int]$Baseline-1] + } + return $true, $Baseline + } + return $false +} + +function Invoke-BaselineItem { + [CmdletBinding()] + param ( + [Parameter()] + [string]$Flag, + [Parameter()] + [string]$Product + ) + + Write-Output "`n==== Testing $Product ====" + foreach($Baseline in $b) { + $Result = Get-Baseline $Baseline + if($Result[0]){ + $Baseline = $Result[1] + $Filename = Get-ChildItem $(Join-Path -Path $FilePath -ChildPath $Product) | + Where-Object {$_.Name -match $('Config2_'+$Baseline+'_test.rego')} | Select-Object Fullname + + if(Test-Path -Path $Filename.Fullname -PathType Leaf) { + Write-Output "`nTesting Baseline $Baseline" + ..\opa_windows_amd64.exe test ..\Rego\ .\$($Filename.Fullname) $Flag + } + else { + Get-ErrorMsg FileIOError, $Filename + } + } + else { + Get-ErrorMsg BaselineItemNumber + } + } + Write-Output "" +} + +function Invoke-TestName { + [CmdletBinding()] + param ( + [Parameter()] + [string]$Flag, + [Parameter()] + [string]$Product, + [Parameter()] + [string]$Baseline + ) + + $Result = Get-Baseline $Baseline + if($Result[0]){ + $Baseline = $Result[1] + $Filename = Get-ChildItem $(Join-Path -Path $FilePath -ChildPath $Product) | + Where-Object {$_.Name -match $('Config2_'+$Baseline+'_test.rego')} | Select-Object Fullname + + if(Test-Path -Path $Filename.Fullname -PathType Leaf) { + Write-Output "`n==== Testing $Product Baseline $Baseline ====" + + foreach($Test in $t) { + Write-Output "`nTesting $Test" + ..\opa_windows_amd64.exe test ..\Rego\ .\$($Filename.Fullname) -r $Test $Flag + } + } + else { + Get-ErrorMsg FileIOError, $Filename + } + } + else { + Get-ErrorMsg BaselineItemNumber + } + Write-Output "" +} + +$pEmpty = $p[0] -eq "" +$bEmpty = $b[0] -eq "" +$tEmpty = $t[0] -eq "" +$Flag = "" + +if ($h.IsPresent) { + Show-Menu +} +if ($v.IsPresent) { + $Flag = "-v" +} +if($pEmpty -and $bEmpty -and $tEmpty) { + $p = @('AAD','Defender','EXO','OneDrive','PowerPlatform','Sharepoint','Teams') + Invoke-Product -Flag $Flag +} +elseif((-not $pEmpty) -and (-not $bEmpty) -and (-not $tEmpty)) { + if (($p.Count -gt 1) -or ($b.Count -gt 1)) { + Write-Output "**WARNING** can only take 1 argument for each: product & baseline item`n...Running test for $($p[0]) and $($b[0]) only" + } + + Invoke-TestName -Flag $Flag -Product $p[0] -Baseline $b[0] +} +elseif((-not $pEmpty) -and (-not $bEmpty) -and $tEmpty) { + if ($p.Count -gt 1) { + Write-Output "**WARNING** can only take 1 argument for product`n...Running test for $($p[0]) only" + } + Invoke-BaselineItem -Flag $Flag -Product $p[0] +} +elseif((-not $pEmpty) -and $bEmpty -and $tEmpty) { + Invoke-Product -Flag $Flag +} +elseif($pEmpty -or $bEmpty -and (-not $tEmpty)) { + Get-ErrorMsg TestNameFlagsMissing +} +elseif($pEmpty -and (-not $bEmpty) -and $tEmpty) { + Get-ErrorMsg BaselineItemFlagMissing +} \ No newline at end of file diff --git a/Testing/Unit/Rego/AAD/AADConfig2_01_test.rego b/Testing/Unit/Rego/AAD/AADConfig2_01_test.rego new file mode 100644 index 0000000000..8daddff03e --- /dev/null +++ b/Testing/Unit/Rego/AAD/AADConfig2_01_test.rego @@ -0,0 +1,198 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_Conditions_Correct if { + ControlNumber := "AAD 2.1" + Requirement := "Legacy authentication SHALL be blocked" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + }, + "ClientAppTypes": ["other", "exchangeActiveSync"] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "enabled", + "DisplayName": "Test block Legacy Authentication" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 conditional access policy(s) found that meet(s) all requirements:
Test block Legacy Authentication" +} + +test_IncludeApplications_Incorrect if { + ControlNumber := "AAD 2.1" + Requirement := "Legacy authentication SHALL be blocked" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["Office365"] + }, + "Users": { + "IncludeUsers": ["All"] + }, + "ClientAppTypes": ["other", "exchangeActiveSync"] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "enabled", + "DisplayName": "Test block Legacy Authentication" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_IncludeUsers_Incorrect if { + ControlNumber := "AAD 2.1" + Requirement := "Legacy authentication SHALL be blocked" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["49b4dcdf-1f90-41a5-9dd7-5e7c3609b423"] + }, + "ClientAppTypes": ["other", "exchangeActiveSync"] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "enabled", + "DisplayName": "Test block Legacy Authentication" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_ClientAppTypes_Incorrect if { + ControlNumber := "AAD 2.1" + Requirement := "Legacy authentication SHALL be blocked" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + }, + "ClientAppTypes": [""] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "enabled", + "DisplayName": "Test block Legacy Authentication" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_BuiltInControls_Incorrect if { + ControlNumber := "AAD 2.1" + Requirement := "Legacy authentication SHALL be blocked" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + }, + "ClientAppTypes": ["other", "exchangeActiveSync"] + }, + "GrantControls": { + "BuiltInControls": null + }, + "State": "enabled", + "DisplayName": "Test block Legacy Authentication" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_State_Incorrect if { + ControlNumber := "AAD 2.1" + Requirement := "Legacy authentication SHALL be blocked" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + }, + "ClientAppTypes": ["other", "exchangeActiveSync"] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "disabled", + "DisplayName": "Test block Legacy Authentication" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/AAD/AADConfig2_02_test.rego b/Testing/Unit/Rego/AAD/AADConfig2_02_test.rego new file mode 100644 index 0000000000..6b44737dae --- /dev/null +++ b/Testing/Unit/Rego/AAD/AADConfig2_02_test.rego @@ -0,0 +1,274 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_Conditions_Correct if { + ControlNumber := "AAD 2.2" + Requirement := "Users detected as high risk SHALL be blocked" + + Output := tests with input as + {"conditional_access_policies": [ + { + "Conditions": { + "Applications": {"IncludeApplications": ["All"]}, + "Users": {"IncludeUsers": ["All"]}, + "UserRiskLevels": ["high"] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "enabled", + "DisplayName": "Test name" + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 conditional access policy(s) found that meet(s) all requirements:
Test name" +} + +test_IncludeApplications_Incorrect if { + ControlNumber := "AAD 2.2" + Requirement := "Users detected as high risk SHALL be blocked" + + Output := tests with input as + {"conditional_access_policies": [ + { + "Conditions": { + "Applications": {"IncludeApplications": ["Office365"]}, + "Users": {"IncludeUsers": ["All"]}, + "UserRiskLevels": ["high"] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "enabled", + "DisplayName": "Test name" + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_IncludeUsers_Incorrect if { + ControlNumber := "AAD 2.2" + Requirement := "Users detected as high risk SHALL be blocked" + + Output := tests with input as + {"conditional_access_policies": [ + { + "Conditions": { + "Applications": {"IncludeApplications": ["All"]}, + "Users": {"IncludeUsers": ["8bc7c6ee-39a2-42a5-a31b-f77fb51db652"]}, + "UserRiskLevels": ["high"] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "enabled", + "DisplayName": "Test name" + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_BuiltInControls_Incorrect if { + ControlNumber := "AAD 2.2" + Requirement := "Users detected as high risk SHALL be blocked" + + Output := tests with input as + {"conditional_access_policies": [ + { + "Conditions": { + "Applications": {"IncludeApplications": ["All"]}, + "Users": {"IncludeUsers": ["All"]}, + "UserRiskLevels": ["high"] + }, + "GrantControls": { + "BuiltInControls": [""] + }, + "State": "enabled", + "DisplayName": "Test name" + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_State_Incorrect if { + ControlNumber := "AAD 2.2" + Requirement := "Users detected as high risk SHALL be blocked" + + Output := tests with input as + {"conditional_access_policies": [ + { + "Conditions": { + "Applications": {"IncludeApplications": ["All"]}, + "Users": {"IncludeUsers": ["All"]}, + "UserRiskLevels": ["high"] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "disabled", + "DisplayName": "Test name" + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_UserRiskLevels_Incorrect if { + ControlNumber := "AAD 2.2" + Requirement := "Users detected as high risk SHALL be blocked" + + Output := tests with input as + {"conditional_access_policies": [ + { + "Conditions": { + "Applications": {"IncludeApplications": ["All"]}, + "Users": {"IncludeUsers": ["All"]}, + "UserRiskLevels": [""] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "enabled", + "DisplayName": "Test name" + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_ServicePlans_Incorrect if { + ControlNumber := "AAD 2.2" + Requirement := "Users detected as high risk SHALL be blocked" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + }, + "UserRiskLevels": [""] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "enabled", + "DisplayName": "Test name" + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "**NOTE: Your tenant does not have an Azure AD Premium P2 license, which is required for this feature**" +} + +# +# Policy 2 +#-- +test_NotImplemented_Correct if { + ControlNumber := "AAD 2.2" + Requirement := "A notification SHOULD be sent to the administrator when high-risk users are detected" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Azure Active Directory Secure Configuration Baseline policy 2.2 for instructions on manual check" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/AAD/AADConfig2_03_test.rego b/Testing/Unit/Rego/AAD/AADConfig2_03_test.rego new file mode 100644 index 0000000000..bdf6f80674 --- /dev/null +++ b/Testing/Unit/Rego/AAD/AADConfig2_03_test.rego @@ -0,0 +1,222 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_Conditions_Correct if { + ControlNumber := "AAD 2.3" + Requirement := "Sign-ins detected as high risk SHALL be blocked" + + Output := tests with input as + {"conditional_access_policies": [ + { + "Conditions": { + "Applications": {"IncludeApplications": ["All"]}, + "Users": {"IncludeUsers": ["All"]}, + "SignInRiskLevels": ["high"] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "enabled", + "DisplayName": "Test name" + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 conditional access policy(s) found that meet(s) all requirements:
Test name" +} + +test_IncludeApplications_Incorrect if { + ControlNumber := "AAD 2.3" + Requirement := "Sign-ins detected as high risk SHALL be blocked" + + Output := tests with input as + {"conditional_access_policies": [ + { + "Conditions": { + "Applications": {"IncludeApplications": ["Office365"]}, + "Users": {"IncludeUsers": ["All"]}, + "SignInRiskLevels": ["high"] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "enabled", + "DisplayName": "Test name" + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_IncludeUsers_Incorrect if { + ControlNumber := "AAD 2.3" + Requirement := "Sign-ins detected as high risk SHALL be blocked" + + Output := tests with input as + {"conditional_access_policies": [ + { + "Conditions": { + "Applications": {"IncludeApplications": ["All"]}, + "Users": {"IncludeUsers": ["8bc7c6ee-39a2-42a5-a31b-f77fb51db652"]}, + "SignInRiskLevels": ["high"] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "enabled", + "DisplayName": "Test name" + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_SignInRiskLevels_Incorrect if { + ControlNumber := "AAD 2.3" + Requirement := "Sign-ins detected as high risk SHALL be blocked" + + Output := tests with input as + {"conditional_access_policies": [ + { + "Conditions": { + "Applications": {"IncludeApplications": ["All"]}, + "Users": {"IncludeUsers": ["All"]}, + "SignInRiskLevels": [""] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "enabled", + "DisplayName": "Test name" + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_BuiltInControls_Incorrect if { + ControlNumber := "AAD 2.3" + Requirement := "Sign-ins detected as high risk SHALL be blocked" + + Output := tests with input as + {"conditional_access_policies": [ + { + "Conditions": { + "Applications": {"IncludeApplications": ["All"]}, + "Users": {"IncludeUsers": ["All"]}, + "SignInRiskLevels": ["high"] + }, + "GrantControls": { + "BuiltInControls": [""] + }, + "State": "enabled", + "DisplayName": "Test name" + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_State_Incorrect if { + ControlNumber := "AAD 2.3" + Requirement := "Sign-ins detected as high risk SHALL be blocked" + + Output := tests with input as + {"conditional_access_policies": [ + { + "Conditions": { + "Applications": {"IncludeApplications": ["All"]}, + "Users": {"IncludeUsers": ["All"]}, + "SignInRiskLevels": ["high"] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "disabled", + "DisplayName": "Test name" + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/AAD/AADConfig2_04_test.rego b/Testing/Unit/Rego/AAD/AADConfig2_04_test.rego new file mode 100644 index 0000000000..8eedc8ac5f --- /dev/null +++ b/Testing/Unit/Rego/AAD/AADConfig2_04_test.rego @@ -0,0 +1,209 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_ConditionalAccessPolicies_Correct if { + ControlNumber := "AAD 2.4" + Requirement := "MFA SHALL be required for all users" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "GrantControls": { + "BuiltInControls": ["mfa"] + }, + "State": "enabled", + "DisplayName": "Test Policy require MFA for All Users" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput)>= 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 conditional access policy(s) found that meet(s) all requirements:
Test Policy require MFA for All Users" +} + +test_IncludeApplications_Incorrect if { + ControlNumber := "AAD 2.4" + Requirement := "MFA SHALL be required for all users" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["Office365"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "GrantControls": { + "BuiltInControls": ["mfa"] + }, + "State": "enabled", + "DisplayName": "Test Policy require MFA for All Users, but not all Apps" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_IncludeUsers_Incorrect if { + ControlNumber := "AAD 2.4" + Requirement := "MFA SHALL be required for all users" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["8bc7c6ee-39a2-42a5-a31b-f77fb51db652"] + } + }, + "GrantControls": { + "BuiltInControls": ["mfa"] + }, + "State": "enabled", + "DisplayName": "Test Policy require MFA for All Apps, but not All Users" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_BuiltInControls_Incorrect if { + ControlNumber := "AAD 2.4" + Requirement := "MFA SHALL be required for all users" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "GrantControls": { + "BuiltInControls": [""] + }, + "State": "enabled", + "DisplayName": "Test Policy does not require MFA" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_State_Incorrect if { + ControlNumber := "AAD 2.4" + Requirement := "MFA SHALL be required for all users" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "GrantControls": { + "BuiltInControls": ["mfa"] + }, + "State": "disabled", + "DisplayName": "Test Policy is correct, but not enabled" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +# +# Policy 2 +#-- +test_NotImplemented_Correct_V1 if { + ControlNumber := "AAD 2.4" + Requirement := "Phishing-resistant MFA SHALL be used for all users" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Azure Active Directory Secure Configuration Baseline policy 2.4 for instructions on manual check" +} + +# +# Policy 3 +#-- +test_NotImplemented_Correct_V2 if { + ControlNumber := "AAD 2.4" + Requirement := "If phishing-resistant MFA cannot be used, an MFA method from the list [see AAD baseline 2.4] SHALL be used in the interim" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Azure Active Directory Secure Configuration Baseline policy 2.4 for instructions on manual check" +} + +# +# Policy 4 +#-- +test_NotImplemented_Correct_V3 if { + ControlNumber := "AAD 2.4" + Requirement := "SMS or Voice as the MFA method SHALL NOT be used" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Azure Active Directory Secure Configuration Baseline policy 2.4 for instructions on manual check" +} diff --git a/Testing/Unit/Rego/AAD/AADConfig2_05_test.rego b/Testing/Unit/Rego/AAD/AADConfig2_05_test.rego new file mode 100644 index 0000000000..529a8a0e9a --- /dev/null +++ b/Testing/Unit/Rego/AAD/AADConfig2_05_test.rego @@ -0,0 +1,35 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_NotImplemented_Correct_V1 if { + ControlNumber := "AAD 2.5" + Requirement := "The following critical logs SHALL be sent at a minimum: AuditLogs, SignInLogs, RiskyUsers, UserRiskEvents, NonInteractiveUserSignInLogs, ServicePrincipalSignInLogs, ADFSSignInLogs, RiskyServicePrincipals, ServicePrincipalRiskEvents" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Azure Active Directory Secure Configuration Baseline policy 2.5 for instructions on manual check" +} + +# +# Policy 2 +#-- +test_NotImplemented_Correct_V2 if { + ControlNumber := "AAD 2.5" + Requirement := "The logs SHALL be sent to the agency's SOC for monitoring" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Azure Active Directory Secure Configuration Baseline policy 2.5 for instructions on manual check" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/AAD/AADConfig2_06_test.rego b/Testing/Unit/Rego/AAD/AADConfig2_06_test.rego new file mode 100644 index 0000000000..428ef8c358 --- /dev/null +++ b/Testing/Unit/Rego/AAD/AADConfig2_06_test.rego @@ -0,0 +1,47 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_AllowedToCreateApps_Correct if { + ControlNumber := "AAD 2.6" + Requirement := "Only administrators SHALL be allowed to register third-party applications" + + Output := tests with input as { + "authorization_policies": { + "DefaultUserRolePermissions": { + "AllowedToCreateApps": false + } + } + } + + # filter for just the output produced by the specific rule by + # checking 1) the control number and 2) the requirement string + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + # Each rule should produce exactly 1 line of output in the report + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowedToCreateApps_Incorrect if { + ControlNumber := "AAD 2.6" + Requirement := "Only administrators SHALL be allowed to register third-party applications" + + Output := tests with input as { + "authorization_policies": { + "DefaultUserRolePermissions": { + "AllowedToCreateApps": true + } + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/AAD/AADConfig2_07_test.rego b/Testing/Unit/Rego/AAD/AADConfig2_07_test.rego new file mode 100644 index 0000000000..df7c69169a --- /dev/null +++ b/Testing/Unit/Rego/AAD/AADConfig2_07_test.rego @@ -0,0 +1,130 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_PermissionGrantPolicyIdsAssignedToDefaultUserRole_Correct if { + ControlNumber := "AAD 2.7" + Requirement := "Only administrators SHALL be allowed to consent to third-party applications" + + Output := tests with input as { + "authorization_policies": { + "PermissionGrantPolicyIdsAssignedToDefaultUserRole": [] + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_PermissionGrantPolicyIdsAssignedToDefaultUserRole_Incorrect if { + ControlNumber := "AAD 2.7" + Requirement := "Only administrators SHALL be allowed to consent to third-party applications" + + Output := tests with input as { + "authorization_policies": { + "PermissionGrantPolicyIdsAssignedToDefaultUserRole": [ + "Test User" + ] + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +# +# Policy 2 +#-- +test_IsEnabled_Correct if { + ControlNumber := "AAD 2.7" + Requirement := "An admin consent workflow SHALL be configured" + + Output := tests with input as { + "admin_consent_policies": { + "IsEnabled" : true + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_IsEnabled_Incorrect if { + ControlNumber := "AAD 2.7" + Requirement := "An admin consent workflow SHALL be configured" + + Output := tests with input as { + "admin_consent_policies": { + "IsEnabled" : false + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +# +# Policy 3 +#-- +test_Value_Correct if { + ControlNumber := "AAD 2.7" + Requirement := "Group owners SHALL NOT be allowed to consent to third-party applications" + + Output := tests with input as { + "directory_settings": [ + { + "Values" : [ + { + "Name" : "EnableGroupSpecificConsent", + "Value" : "false" + } + ] + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Value_Incorrect if { + ControlNumber := "AAD 2.7" + Requirement := "Group owners SHALL NOT be allowed to consent to third-party applications" + + Output := tests with input as { + "directory_settings": [ + { + "Values" : [ + { + "Name" : "EnableGroupSpecificConsent", + "Value" : "true" + } + ] + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/AAD/AADConfig2_08_test.rego b/Testing/Unit/Rego/AAD/AADConfig2_08_test.rego new file mode 100644 index 0000000000..009b21f2b1 --- /dev/null +++ b/Testing/Unit/Rego/AAD/AADConfig2_08_test.rego @@ -0,0 +1,19 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_NotImplemented_Correct if { + ControlNumber := "AAD 2.8" + Requirement := "User passwords SHALL NOT expire" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Azure Active Directory Secure Configuration Baseline policy 2.8 for instructions on manual check" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/AAD/AADConfig2_09_test.rego b/Testing/Unit/Rego/AAD/AADConfig2_09_test.rego new file mode 100644 index 0000000000..cdeca30fe9 --- /dev/null +++ b/Testing/Unit/Rego/AAD/AADConfig2_09_test.rego @@ -0,0 +1,250 @@ +package aad +import future.keywords + +# +# Policy 1 +#-- +test_ConditionalAccessPolicies_Correct if { + ControlNumber := "AAD 2.9" + Requirement := "Sign-in frequency SHALL be configured to 12 hours" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "SessionControls": { + "SignInFrequency": { + "IsEnabled" : true, + "Type" : "hours", + "Value" : 12 + } + }, + "State": "enabled", + "DisplayName" : "Test Name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 conditional access policy(s) found that meet(s) all requirements:
Test Name" +} + +test_IncludeApplications_Incorrect if { + ControlNumber := "AAD 2.9" + Requirement := "Sign-in frequency SHALL be configured to 12 hours" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": [] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "SessionControls": { + "SignInFrequency": { + "IsEnabled" : true, + "Type" : "hours", + "Value" : 12 + } + }, + "State": "enabled", + "DisplayName" : "Test Name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_IncludeUsers_Incorrect if { + ControlNumber := "AAD 2.9" + Requirement := "Sign-in frequency SHALL be configured to 12 hours" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": [] + } + }, + "SessionControls": { + "SignInFrequency": { + "IsEnabled" : true, + "Type" : "hours", + "Value" : 12 + } + }, + "State": "enabled", + "DisplayName" : "Test Name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_IsEnabled_Incorrect if { + ControlNumber := "AAD 2.9" + Requirement := "Sign-in frequency SHALL be configured to 12 hours" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "SessionControls": { + "SignInFrequency": { + "IsEnabled" : false, + "Type" : "hours", + "Value" : 12 + } + }, + "State": "enabled", + "DisplayName" : "Test Name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_Type_Incorrect if { + ControlNumber := "AAD 2.9" + Requirement := "Sign-in frequency SHALL be configured to 12 hours" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "SessionControls": { + "SignInFrequency": { + "IsEnabled" : true, + "Type" : "Hello World", + "Value" : 12 + } + }, + "State": "enabled", + "DisplayName" : "Test Name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_Value_Incorrect if { + ControlNumber := "AAD 2.9" + Requirement := "Sign-in frequency SHALL be configured to 12 hours" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "SessionControls": { + "SignInFrequency": { + "IsEnabled" : true, + "Type" : "hours", + "Value" : 24 + } + }, + "State": "enabled", + "DisplayName" : "Test Name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_State_Incorrect if { + ControlNumber := "AAD 2.9" + Requirement := "Sign-in frequency SHALL be configured to 12 hours" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "SessionControls": { + "SignInFrequency": { + "IsEnabled" : true, + "Type" : "hours", + "Value" : 12 + } + }, + "State": "disabled", + "DisplayName" : "Test Name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/AAD/AADConfig2_10_test.rego b/Testing/Unit/Rego/AAD/AADConfig2_10_test.rego new file mode 100644 index 0000000000..1ccdb28cb2 --- /dev/null +++ b/Testing/Unit/Rego/AAD/AADConfig2_10_test.rego @@ -0,0 +1,210 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_ConditionalAccessPolicies_Correct if { + ControlNumber := "AAD 2.10" + Requirement := "Browser sessions SHALL not be persistent" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "SessionControls": { + "PersistentBrowser": { + "IsEnabled" : true, + "Mode" : "never" + } + }, + "State": "enabled", + "DisplayName" : "Test Name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 conditional access policy(s) found that meet(s) all requirements:
Test Name" +} + +test_IncludeApplications_Incorrect if { + ControlNumber := "AAD 2.10" + Requirement := "Browser sessions SHALL not be persistent" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": [] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "SessionControls": { + "PersistentBrowser": { + "IsEnabled" : true, + "Mode" : "never" + } + }, + "State": "enabled", + "DisplayName" : "Test Name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_IncludeUsers_Incorrect if { + ControlNumber := "AAD 2.10" + Requirement := "Browser sessions SHALL not be persistent" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": [] + } + }, + "SessionControls": { + "PersistentBrowser": { + "IsEnabled" : true, + "Mode" : "never" + } + }, + "State": "enabled", + "DisplayName" : "Test Name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_IsEnabled_Incorrect if { + ControlNumber := "AAD 2.10" + Requirement := "Browser sessions SHALL not be persistent" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "SessionControls": { + "PersistentBrowser": { + "IsEnabled" : false, + "Mode" : "never" + } + }, + "State": "enabled", + "DisplayName" : "Test Name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_Mode_Incorrect if { + ControlNumber := "AAD 2.10" + Requirement := "Browser sessions SHALL not be persistent" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "SessionControls": { + "PersistentBrowser": { + "IsEnabled" : true, + "Mode" : "always" + } + }, + "State": "enabled", + "DisplayName" : "Test Name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_State_Incorrect if { + ControlNumber := "AAD 2.10" + Requirement := "Browser sessions SHALL not be persistent" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "SessionControls": { + "PersistentBrowser": { + "IsEnabled" : true, + "Mode" : "never" + } + }, + "State": "disabled", + "DisplayName" : "Test Name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/AAD/AADConfig2_11_test.rego b/Testing/Unit/Rego/AAD/AADConfig2_11_test.rego new file mode 100644 index 0000000000..829d5cd5e1 --- /dev/null +++ b/Testing/Unit/Rego/AAD/AADConfig2_11_test.rego @@ -0,0 +1,86 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_PrivilegedUsers_Correct if { + ControlNumber := "AAD 2.11" + Requirement := "A minimum of two users and a maximum of four users SHALL be provisioned with the Global Administrator role" + + Output := tests with input as { + "privileged_users" : { + "User1": { + "DisplayName": "Test Name1", + "roles": ["Privileged Role Administrator", "Global Administrator"] + }, + "User2": { + "DisplayName": "Test Name2", + "roles": ["Global Administrator"] + } + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "2 global admin(s) found:
Test Name1, Test Name2" +} + +test_PrivilegedUsers_Incorrect_V1 if { + ControlNumber := "AAD 2.11" + Requirement := "A minimum of two users and a maximum of four users SHALL be provisioned with the Global Administrator role" + + Output := tests with input as { + "privileged_users" : { + "User1": { + "DisplayName": "Test Name1", + "roles": ["Privileged Role Administrator", "Global Administrator"] + } + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 global admin(s) found:
Test Name1" +} + +test_PrivilegedUsers_Incorrect_V2 if { + ControlNumber := "AAD 2.11" + Requirement := "A minimum of two users and a maximum of four users SHALL be provisioned with the Global Administrator role" + + Output := tests with input as { + "privileged_users" : { + "User1": { + "DisplayName": "Test Name1", + "roles": ["Privileged Role Administrator", "Global Administrator"] + }, + "User2": { + "DisplayName": "Test Name2", + "roles": ["Global Administrator"] + }, + "User3": { + "DisplayName": "Test Name3", + "roles": ["Global Administrator"] + }, + "User4": { + "DisplayName": "Test Name4", + "roles": ["Global Administrator"] + }, + "User5": { + "DisplayName": "Test Name5", + "roles": ["Global Administrator"] + } + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "5 global admin(s) found:
Test Name1, Test Name2, Test Name3, Test Name4, Test Name5" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/AAD/AADConfig2_12_test.rego b/Testing/Unit/Rego/AAD/AADConfig2_12_test.rego new file mode 100644 index 0000000000..9a774d1136 --- /dev/null +++ b/Testing/Unit/Rego/AAD/AADConfig2_12_test.rego @@ -0,0 +1,78 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_OnPremisesImmutableId_Correct if { + ControlNumber := "AAD 2.12" + Requirement := "Users that need to be assigned to highly privileged Azure AD roles SHALL be provisioned cloud-only accounts that are separate from the on-premises directory or other federated identity providers" + + Output := tests with input as { + "privileged_users": { + "User1": { + "DisplayName": "Alice", + "OnPremisesImmutableId": null, + "roles": ["Privileged Role Administrator", "Global Administrator"] + }, + "User2": { + "DisplayName": "Bob", + "OnPremisesImmutableId": null, + "roles": ["Global Administrator"] + } + } + } + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 admin(s) that are not cloud-only found" +} + +test_OnPremisesImmutableId_Incorrect_V1 if { + ControlNumber := "AAD 2.12" + Requirement := "Users that need to be assigned to highly privileged Azure AD roles SHALL be provisioned cloud-only accounts that are separate from the on-premises directory or other federated identity providers" + + Output := tests with input as { + "privileged_users": { + "User1": { + "DisplayName": "Alice", + "OnPremisesImmutableId": "HelloWorld", + "roles": ["Privileged Role Administrator", "Global Administrator"] + } + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 admin(s) that are not cloud-only found:
Alice" +} + +test_OnPremisesImmutableId_Incorrect_V2 if { + ControlNumber := "AAD 2.12" + Requirement := "Users that need to be assigned to highly privileged Azure AD roles SHALL be provisioned cloud-only accounts that are separate from the on-premises directory or other federated identity providers" + + Output := tests with input as { + "privileged_users": { + "User1": { + "DisplayName": "Alice", + "OnPremisesImmutableId": "HelloWorld", + "roles": ["Privileged Role Administrator", "Global Administrator"] + }, + "User2": { + "DisplayName": "Bob", + "OnPremisesImmutableId": null, + "roles": ["Global Administrator"] + } + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 admin(s) that are not cloud-only found:
Alice" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/AAD/AADConfig2_13_test.rego b/Testing/Unit/Rego/AAD/AADConfig2_13_test.rego new file mode 100644 index 0000000000..2aab8a1b2d --- /dev/null +++ b/Testing/Unit/Rego/AAD/AADConfig2_13_test.rego @@ -0,0 +1,248 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_ConditionalAccessPolicies_Correct if { + ControlNumber := "AAD 2.13" + Requirement := "MFA SHALL be required for user access to highly privileged roles" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeRoles": ["Role1", "Role2" ] + } + }, + "GrantControls": { + "BuiltInControls": ["mfa"] + }, + "State": "enabled", + "DisplayName": "MFA required for all highly Privileged Roles Policy" + } + ], + "privileged_roles": [ + { + "RoleTemplateId": "Role1", + "DisplayName": "Global Administrator" + }, + { + "RoleTemplateId": "Role2", + "DisplayName": "Privileged Role Administrator" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 conditional access policy(s) found that meet(s) all requirements:
MFA required for all highly Privileged Roles Policy" +} + +test_IncludeApplications_Incorrect if { + ControlNumber := "AAD 2.13" + Requirement := "MFA SHALL be required for user access to highly privileged roles" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": [""] + }, + "Users": { + "IncludeRoles": ["Role1", "Role2" ] + } + }, + "GrantControls": { + "BuiltInControls": ["mfa"] + }, + "State": "enabled", + "DisplayName": {"MFA required for all highly Privileged Roles Policy"} + } + ], + "privileged_roles": [ + { + "RoleTemplateId": "Role1", + "DisplayName": "Global Administrator" + }, + { + "RoleTemplateId": "Role2", + "DisplayName": "Privileged Role Administrator" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_BuiltInControls_Incorrect if { + ControlNumber := "AAD 2.13" + Requirement := "MFA SHALL be required for user access to highly privileged roles" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeRoles": ["Role1", "Role2" ] + } + }, + "GrantControls": { + "BuiltInControls": [""] + }, + "State": "enabled", + "DisplayName": {"MFA required for all highly Privileged Roles Policy"} + } + ], + "privileged_roles": [ + { + "RoleTemplateId": "Role1", + "DisplayName": "Global Administrator" + }, + { + "RoleTemplateId": "Role2", + "DisplayName": "Privileged Role Administrator" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_State_Incorrect if { + ControlNumber := "AAD 2.13" + Requirement := "MFA SHALL be required for user access to highly privileged roles" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeRoles": ["Role1", "Role2" ] + } + }, + "GrantControls": { + "BuiltInControls": ["mfa"] + }, + "State": "disabled", + "DisplayName": {"MFA required for all highly Privileged Roles Policy"} + } + ], + "privileged_roles": [ + { + "RoleTemplateId": "Role1", + "DisplayName": "Global Administrator" + }, + { + "RoleTemplateId": "Role2", + "DisplayName": "Privileged Role Administrator" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_IncludeRoles_Incorrect_V1 if { + ControlNumber := "AAD 2.13" + Requirement := "MFA SHALL be required for user access to highly privileged roles" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeRoles": ["Role1"] + } + }, + "GrantControls": { + "BuiltInControls": ["mfa"] + }, + "State": "enabled", + "DisplayName": {"MFA required for all highly Privileged Roles Policy"} + } + ], + "privileged_roles": [ + { + "RoleTemplateId": "Role1", + "DisplayName": "Global Administrator" + }, + { + "RoleTemplateId": "Role2", + "DisplayName": "Privileged Role Administrator" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_IncludeRoles_Incorrect_V2 if { + ControlNumber := "AAD 2.13" + Requirement := "MFA SHALL be required for user access to highly privileged roles" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeRoles": ["Role2" ] + } + }, + "GrantControls": { + "BuiltInControls": ["mfa"] + }, + "State": "enabled", + "DisplayName": {"MFA required for all highly Privileged Roles Policy"} + } + ], + "privileged_roles": [ + { + "RoleTemplateId": "Role1", + "DisplayName": "Global Administrator" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/AAD/AADConfig2_14_test.rego b/Testing/Unit/Rego/AAD/AADConfig2_14_test.rego new file mode 100644 index 0000000000..5e8df833a3 --- /dev/null +++ b/Testing/Unit/Rego/AAD/AADConfig2_14_test.rego @@ -0,0 +1,199 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_AdditionalProperties_Correct if { + ControlNumber := "AAD 2.14" + Requirement := "Permanent active role assignments SHALL NOT be allowed for highly privileged roles. Active assignments SHALL have an expiration period." + + Output := tests with input as { + "privileged_roles": [ + { + "DisplayName": "Global Administrator", + "Rules": [ + { + "Id": "Expiration_Admin_Assignment", + "AdditionalProperties": { + "isExpirationRequired": true, + "maximumDuration": "P15D" + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 role(s) configured to allow permanent active assignment or expiration period too long" +} + +test_AdditionalProperties_Incorrect_V1 if { + ControlNumber := "AAD 2.14" + Requirement := "Permanent active role assignments SHALL NOT be allowed for highly privileged roles. Active assignments SHALL have an expiration period." + + Output := tests with input as { + "privileged_roles": [ + { + "DisplayName": "Global Administrator", + "Rules": [ + { + "Id": "Expiration_Admin_Assignment", + "AdditionalProperties": { + "isExpirationRequired": false, + "maximumDuration": "P30D" + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 role(s) configured to allow permanent active assignment or expiration period too long:
Global Administrator" +} + +test_AdditionalProperties_Incorrect_V2 if { + ControlNumber := "AAD 2.14" + Requirement := "Permanent active role assignments SHALL NOT be allowed for highly privileged roles. Active assignments SHALL have an expiration period." + + Output := tests with input as { + "privileged_roles": [ + { + "DisplayName": "Global Administrator", + "Rules": [ + { + "Id": "Expiration_Admin_Assignment", + "AdditionalProperties": { + "isExpirationRequired": true, + "maximumDuration": "P30D" + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 role(s) configured to allow permanent active assignment or expiration period too long:
Global Administrator" +} + +# +# Policy 2 +#-- +test_Assignments_Correct if { + ControlNumber := "AAD 2.14" + Requirement := "Provisioning of users to highly privileged roles SHALL NOT occur outside of a PAM system, such as the Azure AD PIM service, because this bypasses the controls the PAM system provides" + + Output := tests with input as { + "privileged_roles": [ + { + "DisplayName": "Global Administrator", + "Assignments": [ + { + "StartDateTime": "/Date(1660328610000)/" + } + ], + "Rules": [ + { + "Id": "Expiration_Admin_Assignment", + "AdditionalProperties": { + "isExpirationRequired": true, + "maximumDuration": "P30D" + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 role(s) assigned to users outside of PIM" +} + +test_Assignments_Incorrect if { + ControlNumber := "AAD 2.14" + Requirement := "Provisioning of users to highly privileged roles SHALL NOT occur outside of a PAM system, such as the Azure AD PIM service, because this bypasses the controls the PAM system provides" + + Output := tests with input as { + "privileged_roles": [ + { + "DisplayName": "Global Administrator", + "Assignments": [ + { + "StartDateTime": null + } + ], + "Rules": [ + { + "Id": "Expiration_Admin_Assignment", + "AdditionalProperties": { + "isExpirationRequired": true, + "maximumDuration": "P30D" + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 role(s) assigned to users outside of PIM:
Global Administrator" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/AAD/AADConfig2_15_test.rego b/Testing/Unit/Rego/AAD/AADConfig2_15_test.rego new file mode 100644 index 0000000000..ad64ed79ea --- /dev/null +++ b/Testing/Unit/Rego/AAD/AADConfig2_15_test.rego @@ -0,0 +1,80 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_AdditionalProperties_Correct if { + ControlNumber := "AAD 2.15" + Requirement := "Activation of highly privileged roles SHOULD require approval" + + Output := tests with input as { + "privileged_roles": [ + { + "DisplayName": "Global Administrator", + "Rules": [ + { + "Id": "Approval_EndUser_Assignment", + "AdditionalProperties": { + "setting": { + "isApprovalRequired" : true + } + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 role(s) that do not require approval to activate found" +} + +test_AdditionalProperties_Incorrect if { + ControlNumber := "AAD 2.15" + Requirement := "Activation of highly privileged roles SHOULD require approval" + + Output := tests with input as { + "privileged_roles": [ + { + "DisplayName": "Global Administrator", + "Rules": [ + { + "Id": "Approval_EndUser_Assignment", + "AdditionalProperties": { + "setting": { + "isApprovalRequired" : false + } + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 role(s) that do not require approval to activate found:
Global Administrator" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/AAD/AADConfig2_16_test.rego b/Testing/Unit/Rego/AAD/AADConfig2_16_test.rego new file mode 100644 index 0000000000..9f39cb47a5 --- /dev/null +++ b/Testing/Unit/Rego/AAD/AADConfig2_16_test.rego @@ -0,0 +1,359 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_notificationRecipients_Correct if { + ControlNumber := "AAD 2.16" + Requirement := "Eligible and Active highly privileged role assignments SHALL trigger an alert" + + Output := tests with input as { + "privileged_roles": [ + { + "RoleTemplateId": "1D2EE3F0-90D3-4764-8AF8-BE81FE9D4D71", + "DisplayName": "Global Administrator", + "Rules": [ + { + "Id": "Notification_Admin_Admin_Assignment", + "AdditionalProperties": { + "notificationRecipients": ["test@example.com"] + } + }, + { + "Id": "Notification_Admin_Admin_Eligibility", + "AdditionalProperties": { + "notificationRecipients": ["test@example.com"] + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 role(s) without notification e-mail configured for role assignments found" +} + +test_notificationRecipients_Incorrect_V1 if { + ControlNumber := "AAD 2.16" + Requirement := "Eligible and Active highly privileged role assignments SHALL trigger an alert" + + Output := tests with input as { + "privileged_roles": [ + { + "RoleTemplateId": "1D2EE3F0-90D3-4764-8AF8-BE81FE9D4D71", + "DisplayName": "Global Administrator", + "Rules": [ + { + "Id": "Notification_Admin_Admin_Assignment", + "AdditionalProperties": { + "notificationRecipients": [] + } + }, + { + "Id": "Notification_Admin_Admin_Eligibility", + "AdditionalProperties": { + "notificationRecipients": ["test@example.com"] + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 role(s) without notification e-mail configured for role assignments found:
Global Administrator" +} + +test_notificationRecipients_Incorrect_V2 if { + ControlNumber := "AAD 2.16" + Requirement := "Eligible and Active highly privileged role assignments SHALL trigger an alert" + + Output := tests with input as { + "privileged_roles": [ + { + "RoleTemplateId": "1D2EE3F0-90D3-4764-8AF8-BE81FE9D4D71", + "DisplayName": "Global Administrator", + "Rules": [ + { + "Id": "Notification_Admin_Admin_Assignment", + "AdditionalProperties": { + "notificationRecipients": ["test@example.com"] + } + }, + { + "Id": "Notification_Admin_Admin_Eligibility", + "AdditionalProperties": { + "notificationRecipients": [] + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 role(s) without notification e-mail configured for role assignments found:
Global Administrator" +} + +test_notificationRecipients_Incorrect_V3 if { + ControlNumber := "AAD 2.16" + Requirement := "Eligible and Active highly privileged role assignments SHALL trigger an alert" + + Output := tests with input as { + "privileged_roles": [ + { + "RoleTemplateId": "1D2EE3F0-90D3-4764-8AF8-BE81FE9D4D71", + "DisplayName": "Global Administrator", + "Rules": [ + { + "Id": "Notification_Admin_Admin_Assignment", + "AdditionalProperties": { + "notificationRecipients": [] + } + }, + { + "Id": "Notification_Admin_Admin_Eligibility", + "AdditionalProperties": { + "notificationRecipients": [] + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 role(s) without notification e-mail configured for role assignments found:
Global Administrator" +} + +test_Id_Correct_V1 if { + ControlNumber := "AAD 2.16" + Requirement := "User activation of the Global Administrator role SHALL trigger an alert" + + Output := tests with input as { + "privileged_roles": [ + { + "RoleTemplateId": "1D2EE3F0-90D3-4764-8AF8-BE81FE9D4D71", + "DisplayName": "Global Administrator", + "Rules": [ + { + "Id": "Notification_Admin_EndUser_Assignment", + "AdditionalProperties": { + "notificationType": "Email", + "notificationRecipients": ["test@example.com"] + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Id_Correct_V2 if { + ControlNumber := "AAD 2.16" + Requirement := "User activation of the Global Administrator role SHALL trigger an alert" + + Output := tests with input as { + "privileged_roles": [ + { + "RoleTemplateId": "1D2EE3F0-90D3-4764-8AF8-BE81FE9D4D71", + "DisplayName": "Global Administrator", + "Rules": [ + { + "Id": "Notification_Admin_EndUser_Assignment", + "AdditionalProperties": { + "notificationType": "", + "notificationRecipients": ["test@example.com"] + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Id_Incorrect if { + ControlNumber := "AAD 2.16" + Requirement := "User activation of the Global Administrator role SHALL trigger an alert" + + Output := tests with input as { + "privileged_roles": [ + { + "RoleTemplateId": "1D2EE3F0-90D3-4764-8AF8-BE81FE9D4D71", + "DisplayName": "Global Administrator", + "Rules": [ + { + "Id": "Notification_Admin_EndUser_Assignment", + "AdditionalProperties": { + "notificationType": "Email", + "notificationRecipients": [] + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_DisplayName_Correct if { + ControlNumber := "AAD 2.16" + Requirement := "User activation of other highly privileged roles SHOULD trigger an alert" + + Output := tests with input as { + "privileged_roles": [ + { + "RoleTemplateId": "1D2EE3F0-90D3-4764-8AF8-BE81FE9D4D71", + "DisplayName": "Global Administrator", + "Rules": [ + { + "Id": "Notification_Admin_EndUser_Assignment", + "AdditionalProperties": { + "notificationType": "Email", + "notificationRecipients": [] + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 role(s) without notification e-mail configured for role activations found" +} + +test_DisplayName_Incorrect if { + ControlNumber := "AAD 2.16" + Requirement := "User activation of other highly privileged roles SHOULD trigger an alert" + + Output := tests with input as { + "privileged_roles": [ + { + "RoleTemplateId": "1D2EE3F0-90D3-4764-8AF8-BE81FE9D4D71", + "DisplayName": "Cloud Administrator", + "Rules": [ + { + "Id": "Notification_Admin_EndUser_Assignment", + "AdditionalProperties": { + "notificationType": "Email", + "notificationRecipients": [] + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 role(s) without notification e-mail configured for role activations found:
Cloud Administrator" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/AAD/AADConfig2_17_test.rego b/Testing/Unit/Rego/AAD/AADConfig2_17_test.rego new file mode 100644 index 0000000000..363855eb48 --- /dev/null +++ b/Testing/Unit/Rego/AAD/AADConfig2_17_test.rego @@ -0,0 +1,192 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_ConditionalAccessPolicies_Correct if { + ControlNumber := "AAD 2.17" + Requirement := "Managed devices SHOULD be required for authentication" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "GrantControls": { + "BuiltInControls": ["domainJoinedDevice"] + }, + "State": "enabled", + "DisplayName": "AD Joined Device Authentication Policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 conditional access policy(s) found that meet(s) all requirements:
AD Joined Device Authentication Policy" +} + +test_BuiltInControls_Correct if { + ControlNumber := "AAD 2.17" + Requirement := "Managed devices SHOULD be required for authentication" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "GrantControls": { + "BuiltInControls": ["compliantDevice"] + }, + "State": "enabled", + "DisplayName": "AD Joined Device Authentication Policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 conditional access policy(s) found that meet(s) all requirements:
AD Joined Device Authentication Policy" +} + +test_IncludeApplications_Incorrect if { + ControlNumber := "AAD 2.17" + Requirement := "Managed devices SHOULD be required for authentication" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": [""] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "GrantControls": { + "BuiltInControls": ["compliantDevice"] + }, + "State": "enabled", + "DisplayName": "AD Joined Device Authentication Policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_IncludeUsers_Incorrect if { + ControlNumber := "AAD 2.17" + Requirement := "Managed devices SHOULD be required for authentication" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": [""] + } + }, + "GrantControls": { + "BuiltInControls": ["compliantDevice"] + }, + "State": "enabled", + "DisplayName": "AD Joined Device Authentication Policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_BuiltInControls_Incorrect if { + ControlNumber := "AAD 2.17" + Requirement := "Managed devices SHOULD be required for authentication" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "GrantControls": { + "BuiltInControls": [""] + }, + "State": "enabled", + "DisplayName": "AD Joined Device Authentication Policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_State_Incorrect if { + ControlNumber := "AAD 2.17" + Requirement := "Managed devices SHOULD be required for authentication" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "GrantControls": { + "BuiltInControls": ["compliantDevice"] + }, + "State": "disabled", + "DisplayName": "AD Joined Device Authentication Policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/AAD/AADConfig2_18_test.rego b/Testing/Unit/Rego/AAD/AADConfig2_18_test.rego new file mode 100644 index 0000000000..c745245a46 --- /dev/null +++ b/Testing/Unit/Rego/AAD/AADConfig2_18_test.rego @@ -0,0 +1,133 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_AllowInvitesFrom_Correct if { + ControlNumber := "AAD 2.18" + Requirement := "Only users with the Guest Inviter role SHOULD be able to invite guest users" + + Output := tests with input as { + "authorization_policies": + { + "AllowInvitesFrom": "adminsAndGuestInviters" + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowInvitesFrom_Incorrect if { + ControlNumber := "AAD 2.18" + Requirement := "Only users with the Guest Inviter role SHOULD be able to invite guest users" + + Output := tests with input as { + "authorization_policies": + { + "AllowInvitesFrom": "" + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +# +# Policy 2 +#-- +test_NotImplemented_Correct if { + ControlNumber := "AAD 2.18" + Requirement := "Guest invites SHOULD only be allowed to specific external domains that have been authorized by the agency for legitimate business purposes" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Azure Active Directory Secure Configuration Baseline policy 2.18 for instructions on manual check" +} + +# +# Policy 3 +#-- +test_GuestUserRoleId_Correct_V1 if { + ControlNumber := "AAD 2.18" + Requirement := "Guest users SHOULD have limited access to Azure AD directory objects" + + Output := tests with input as { + "authorization_policies": + { + "GuestUserRoleId" : "2af84b1e-32c8-42b7-82bc-daa82404023b" + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Permission level set to \"Restricted access\"" +} + +test_GuestUserRoleId_Correct_V2 if { + ControlNumber := "AAD 2.18" + Requirement := "Guest users SHOULD have limited access to Azure AD directory objects" + + Output := tests with input as { + "authorization_policies": + { + "GuestUserRoleId" : "10dae51f-b6af-4016-8d66-8c2a99b929b3" + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Permission level set to \"Limited access\"" +} + +test_GuestUserRoleId_Incorrect_V1 if { + ControlNumber := "AAD 2.18" + Requirement := "Guest users SHOULD have limited access to Azure AD directory objects" + + Output := tests with input as { + "authorization_policies": + { + "GuestUserRoleId" : "a0b1b346-4d3e-4e8b-98f8-753987be4970" + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Permission level set to \"Same as member users\"" +} + +test_GuestUserRoleId_Incorrect_V2 if { + ControlNumber := "AAD 2.18" + Requirement := "Guest users SHOULD have limited access to Azure AD directory objects" + + Output := tests with input as { + "authorization_policies": + { + "GuestUserRoleId" : "Hello World" + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Permission level set to \"Unknown\"" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/Defender/DefenderConfig2_01_test.rego b/Testing/Unit/Rego/Defender/DefenderConfig2_01_test.rego new file mode 100644 index 0000000000..c3e9dbcacf --- /dev/null +++ b/Testing/Unit/Rego/Defender/DefenderConfig2_01_test.rego @@ -0,0 +1,119 @@ +package defender +import future.keywords + + +# +# Policy 1 +#-- +test_Identity_Correct_V1 if { + ControlNumber := "Defender 2.1" + Requirement := "Standard Preset security profiles SHOULD NOT be used" + + Output := tests with input as { + "protection_policy_rules" : [] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Identity_Correct_V2 if { + ControlNumber := "Defender 2.1" + Requirement := "Standard Preset security profiles SHOULD NOT be used" + + Output := tests with input as { + "protection_policy_rules" : [ + { + "Identity" : "Standard Preset Security Policy", + "State" : "Disabled" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Identity_Incorrect_V1 if { + ControlNumber := "Defender 2.1" + Requirement := "Standard Preset security profiles SHOULD NOT be used" + + Output := tests with input as { + "protection_policy_rules" : [ + { + "Identity" : "Standard Preset Security Policy", + "State" : "Enabled" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "The Standard Preset Security Policy is present and not disabled" +} + +# +# Policy 2 +#-- +test_Identity_Correct_V1 if { + ControlNumber := "Defender 2.1" + Requirement := "Strict Preset security profiles SHOULD NOT be used" + + Output := tests with input as { + "protection_policy_rules" : [] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Identity_Correct_V2 if { + ControlNumber := "Defender 2.1" + Requirement := "Strict Preset security profiles SHOULD NOT be used" + + Output := tests with input as { + "protection_policy_rules" : [ + { + "Identity" : "Strict Preset Security Policy", + "State" : "Disabled" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Identity_Incorrect_V2 if { + ControlNumber := "Defender 2.1" + Requirement := "Strict Preset security profiles SHOULD NOT be used" + + Output := tests with input as { + "protection_policy_rules" : [ + { + "Identity" : "Strict Preset Security Policy", + "State" : "Enabled" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "The Strict Preset Security Policy is present and not disabled" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/Defender/DefenderConfig2_02_test.rego b/Testing/Unit/Rego/Defender/DefenderConfig2_02_test.rego new file mode 100644 index 0000000000..8f2b1b8706 --- /dev/null +++ b/Testing/Unit/Rego/Defender/DefenderConfig2_02_test.rego @@ -0,0 +1,888 @@ +package defender +import future.keywords + + +# +# Policy 1 +#-- +test_ContentContainsSensitiveInformation_Correct_V1 if { + ControlNumber := "Defender 2.2" + Requirement := "A custom policy SHALL be configured to protect PII and sensitive information, as defined by the agency: U.S. Social Security Number (SSN)" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ContentContainsSensitiveInformation_Incorrect_V1 if { + ControlNumber := "Defender 2.2" + Requirement := "A custom policy SHALL be configured to protect PII and sensitive information, as defined by the agency: U.S. Social Security Number (SSN)" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"}, + {"name": "Credit Card Number"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No matching rule found for U.S. Social Security Number (SSN)" +} + +test_ContentContainsSensitiveInformation_Correct_V2 if { + ControlNumber := "Defender 2.2" + Requirement := "A custom policy SHALL be configured to protect PII and sensitive information, as defined by the agency: U.S. Individual Taxpayer Identification Number (ITIN)" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ContentContainsSensitiveInformation_Incorrect_V2 if { + ControlNumber := "Defender 2.2" + Requirement := "A custom policy SHALL be configured to protect PII and sensitive information, as defined by the agency: U.S. Individual Taxpayer Identification Number (ITIN)" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "Credit Card Number"}, + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No matching rule found for U.S. Individual Taxpayer Identification Number (ITIN)" +} + +test_ContentContainsSensitiveInformation_Correct_V3 if { + ControlNumber := "Defender 2.2" + Requirement := "A custom policy SHALL be configured to protect PII and sensitive information, as defined by the agency: Credit Card Number" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "Credit Card Number"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ContentContainsSensitiveInformation_Incorrect_V3 if { + ControlNumber := "Defender 2.2" + Requirement := "A custom policy SHALL be configured to protect PII and sensitive information, as defined by the agency: Credit Card Number" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"}, + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No matching rule found for Credit Card Number" +} + +# +# Policy 2 +#-- +test_Exchange_Correct if { + ControlNumber := "Defender 2.2" + Requirement := "The custom policy SHOULD be applied in Exchange" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"}, + {"name": "Credit Card Number"}, + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ], + "dlp_compliance_policies": [ + { + "ExchangeLocation": ["All"], + "Workload": "Exchange", + "Name": "Default Office 365 DLP policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ExchangeLocation_Incorrect if { + ControlNumber := "Defender 2.2" + Requirement := "The custom policy SHOULD be applied in Exchange" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"}, + {"name": "Credit Card Number"}, + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ], + "dlp_compliance_policies": [ + { + "ExchangeLocation": [""], + "Workload": "Exchange", + "Name": "Default Office 365 DLP policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No policy found that applies to Exchange." +} + +test_Workload_Incorrect_V1 if { + ControlNumber := "Defender 2.2" + Requirement := "The custom policy SHOULD be applied in Exchange" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"}, + {"name": "Credit Card Number"}, + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ], + "dlp_compliance_policies": [ + { + "ExchangeLocation": ["All"], + "Workload": "", + "Name": "Default Office 365 DLP policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No policy found that applies to Exchange." +} + +test_SharePoint_Correct if { + ControlNumber := "Defender 2.2" + Requirement := "The custom policy SHOULD be applied in SharePoint" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"}, + {"name": "Credit Card Number"}, + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ], + "dlp_compliance_policies": [ + { + "SharePointLocation": ["All"], + "Workload": "SharePoint", + "Name": "Default Office 365 DLP policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_SharePointLocation_Incorrect if { + ControlNumber := "Defender 2.2" + Requirement := "The custom policy SHOULD be applied in SharePoint" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"}, + {"name": "Credit Card Number"}, + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ], + "dlp_compliance_policies": [ + { + "SharePointLocation": [""], + "Workload": "SharePoint", + "Name": "Default Office 365 DLP policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No policy found that applies to SharePoint." +} + +test_Workload_Incorrect_V2 if { + ControlNumber := "Defender 2.2" + Requirement := "The custom policy SHOULD be applied in SharePoint" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"}, + {"name": "Credit Card Number"}, + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ], + "dlp_compliance_policies": [ + { + "SharePointLocation": ["All"], + "Workload": "", + "Name": "Default Office 365 DLP policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No policy found that applies to SharePoint." +} + +test_OneDrive_Correct if { + ControlNumber := "Defender 2.2" + Requirement := "The custom policy SHOULD be applied in OneDrive" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"}, + {"name": "Credit Card Number"}, + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ], + "dlp_compliance_policies": [ + { + "OneDriveLocation": ["All"], + "Workload": "OneDrivePoint", + "Name": "Default Office 365 DLP policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_OneDriveLocation_Incorrect if { + ControlNumber := "Defender 2.2" + Requirement := "The custom policy SHOULD be applied in OneDrive" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"}, + {"name": "Credit Card Number"}, + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ], + "dlp_compliance_policies": [ + { + "OneDriveLocation": [""], + "Workload": "OneDrivePoint", + "Name": "Default Office 365 DLP policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No policy found that applies to OneDrive." +} + +test_Workload_Incorrect_V3 if { + ControlNumber := "Defender 2.2" + Requirement := "The custom policy SHOULD be applied in OneDrive" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"}, + {"name": "Credit Card Number"}, + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ], + "dlp_compliance_policies": [ + { + "OneDriveLocation": ["All"], + "Workload": "", + "Name": "Default Office 365 DLP policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No policy found that applies to OneDrive." +} + +test_Teams_Correct if { + ControlNumber := "Defender 2.2" + Requirement := "The custom policy SHOULD be applied in Teams" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"}, + {"name": "Credit Card Number"}, + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ], + "dlp_compliance_policies": [ + { + "TeamsLocation": ["All"], + "Workload": "Teams", + "Name": "Default Office 365 DLP policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_TeamsLocation_Incorrect if { + ControlNumber := "Defender 2.2" + Requirement := "The custom policy SHOULD be applied in Teams" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"}, + {"name": "Credit Card Number"}, + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ], + "dlp_compliance_policies": [ + { + "TeamsLocation": [""], + "Workload": "Teams", + "Name": "Default Office 365 DLP policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No policy found that applies to Teams." +} + +test_Workload_Incorrect_V4 if { + ControlNumber := "Defender 2.2" + Requirement := "The custom policy SHOULD be applied in Teams" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"}, + {"name": "Credit Card Number"}, + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ], + "dlp_compliance_policies": [ + { + "TeamsLocation": ["All"], + "Workload": "", + "Name": "Default Office 365 DLP policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No policy found that applies to Teams." +} + +# +# Policy 3 +#-- +test_BlockAccess_Correct if { + ControlNumber := "Defender 2.2" + Requirement := "The action for the DLP policy SHOULD be set to block sharing sensitive information with everyone when DLP conditions are met" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_BlockAccess_Incorrect if { + ControlNumber := "Defender 2.2" + Requirement := "The action for the DLP policy SHOULD be set to block sharing sensitive information with everyone when DLP conditions are met" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": false, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 rule(s) found that do(es) not block access: Baseline Rule" +} + +# +# Policy 4 +#-- +test_NotifyUser_Correct_V1 if { + ControlNumber := "Defender 2.2" + Requirement := "Notifications to inform users and help educate them on the proper use of sensitive information SHOULD be enabled" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin" + ], + "NotifyUserType": "NotSet" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_NotifyUser_Correct_V2 if { + ControlNumber := "Defender 2.2" + Requirement := "Notifications to inform users and help educate them on the proper use of sensitive information SHOULD be enabled" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_NotifyUser_Incorrect if { + ControlNumber := "Defender 2.2" + Requirement := "Notifications to inform users and help educate them on the proper use of sensitive information SHOULD be enabled" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ ], + "NotifyUserType": "NotSet" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 rule(s) found that do(es) not notify at least one user: Baseline Rule" +} + +# +# Policy 5 +#-- +test_NotImplemented_Correct_V1 if { + ControlNumber := "Defender 2.2" + Requirement := "A list of apps that are not allowed to access files protected by DLP policy SHOULD be defined" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Defender Secure Configuration Baseline policy 2.2 for instructions on manual check" +} + +# +# Policy 6 +#-- +test_NotImplemented_Correct_V2 if { + ControlNumber := "Defender 2.2" + Requirement := "A list of browsers that are not allowed to access files protected by DLP policy SHOULD be defined" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Defender Secure Configuration Baseline policy 2.2 for instructions on manual check" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/Defender/DefenderConfig2_03_test.rego b/Testing/Unit/Rego/Defender/DefenderConfig2_03_test.rego new file mode 100644 index 0000000000..ad1b82b303 --- /dev/null +++ b/Testing/Unit/Rego/Defender/DefenderConfig2_03_test.rego @@ -0,0 +1,198 @@ +package defender +import future.keywords + + +# +# Policy 1 +#-- +test_EnableFileFilter_Correct if { + ControlNumber := "Defender 2.3" + Requirement := "The common attachments filter SHALL be enabled in the default anti-malware policy and in all existing policies" + + Output := tests with input as { + "malware_filter_policies": [ + { + "EnableFileFilter" : true, + "Name": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableFileFilter_Incorrect if { + ControlNumber := "Defender 2.3" + Requirement := "The common attachments filter SHALL be enabled in the default anti-malware policy and in all existing policies" + + Output := tests with input as { + "malware_filter_policies": [ + { + "EnableFileFilter" : false, + "Name": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 malware policy(ies) found that do(es) not have the common attachments filter enabled: Default" +} + +test_EnableFileFilterMultiple_Incorrect if { + ControlNumber := "Defender 2.3" + Requirement := "The common attachments filter SHALL be enabled in the default anti-malware policy and in all existing policies" + + Output := tests with input as { + "malware_filter_policies": [ + { + "EnableFileFilter" : true, + "Name": "Default" + }, + { + "EnableFileFilter" : false, + "Name": "Custom 1" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 malware policy(ies) found that do(es) not have the common attachments filter enabled: Custom 1" +} + +# +# Policy 2 +#-- +test_FileTypes_Correct_V1 if { + ControlNumber := "Defender 2.3" + Requirement := "Disallowed file types SHALL be determined and set. At a minimum, click-to-run files SHOULD be blocked: exe files" + + Output := tests with input as { + "malware_filter_policies": [ + { + "FileTypes" : ["exe"], + "EnableFileFilter" : true, + "Name": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_FileTypes_Incorrect_V1 if { + ControlNumber := "Defender 2.3" + Requirement := "Disallowed file types SHALL be determined and set. At a minimum, click-to-run files SHOULD be blocked: exe files" + + Output := tests with input as { + "malware_filter_policies": [ + { + "FileTypes" : ["cmd", "vbe"], + "EnableFileFilter" : true, + "Name": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No malware policies found that block .exe files." +} + +test_FileTypes_Correct_V2 if { + ControlNumber := "Defender 2.3" + Requirement := "Disallowed file types SHALL be determined and set. At a minimum, click-to-run files SHOULD be blocked: cmd files" + + Output := tests with input as { + "malware_filter_policies": [ + { + "FileTypes" : ["cmd"], + "EnableFileFilter" : true, + "Name": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_FileTypes_Incorrect_V2 if { + ControlNumber := "Defender 2.3" + Requirement := "Disallowed file types SHALL be determined and set. At a minimum, click-to-run files SHOULD be blocked: cmd files" + + Output := tests with input as { + "malware_filter_policies": [ + { + "FileTypes" : ["exe", "vbe"], + "EnableFileFilter" : true, + "Name": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No malware policies found that block .cmd files." +} + +test_FileTypes_Correct_V3 if { + ControlNumber := "Defender 2.3" + Requirement := "Disallowed file types SHALL be determined and set. At a minimum, click-to-run files SHOULD be blocked: vbe files" + + Output := tests with input as { + "malware_filter_policies": [ + { + "FileTypes" : ["vbe"], + "EnableFileFilter" : true, + "Name": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_FileTypes_Incorrect_V3 if { + ControlNumber := "Defender 2.3" + Requirement := "Disallowed file types SHALL be determined and set. At a minimum, click-to-run files SHOULD be blocked: vbe files" + + Output := tests with input as { + "malware_filter_policies": [ + { + "FileTypes" : ["exe", "cmd"], + "EnableFileFilter" : true, + "Name": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No malware policies found that block .vbe files." +} \ No newline at end of file diff --git a/Testing/Unit/Rego/Defender/DefenderConfig2_04_test.rego b/Testing/Unit/Rego/Defender/DefenderConfig2_04_test.rego new file mode 100644 index 0000000000..5bc280fd0e --- /dev/null +++ b/Testing/Unit/Rego/Defender/DefenderConfig2_04_test.rego @@ -0,0 +1,70 @@ +package defender +import future.keywords + + +# +# Policy 1 +#-- +test_ZapEnabled_Correct if { + ControlNumber := "Defender 2.4" + Requirement := "Zero-hour Auto Purge (ZAP) for malware SHOULD be enabled in the default anti-malware policy and in all existing custom policies" + + Output := tests with input as { + "malware_filter_policies": [ + { + "ZapEnabled" : true, + "Name": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ZapEnabled_Incorrect if { + ControlNumber := "Defender 2.4" + Requirement := "Zero-hour Auto Purge (ZAP) for malware SHOULD be enabled in the default anti-malware policy and in all existing custom policies" + + Output := tests with input as { + "malware_filter_policies": [ + { + "ZapEnabled" : false, + "Name": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 malware policy(ies) found without ZAP for malware enabled: Default" +} + +test_ZapEnabledMultiple_Incorrect if { + ControlNumber := "Defender 2.4" + Requirement := "Zero-hour Auto Purge (ZAP) for malware SHOULD be enabled in the default anti-malware policy and in all existing custom policies" + + Output := tests with input as { + "malware_filter_policies": [ + { + "ZapEnabled" : true, + "Name": "Default" + }, + { + "ZapEnabled" : false, + "Name": "Custom 1" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 malware policy(ies) found without ZAP for malware enabled: Custom 1" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/Defender/DefenderConfig2_05_test.rego b/Testing/Unit/Rego/Defender/DefenderConfig2_05_test.rego new file mode 100644 index 0000000000..b24ed52f0a --- /dev/null +++ b/Testing/Unit/Rego/Defender/DefenderConfig2_05_test.rego @@ -0,0 +1,1377 @@ +package defender +import future.keywords + + +# +# Policy 1 +#-- +test_TargetedUsers_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "User impersonation protection SHOULD be enabled for key agency leaders" + + Output := tests with input as { + "anti_phish_policies": [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : true, + "EnableTargetedUserProtection" : true, + "TargetedUsersToProtect" : [ + "john doe;jdoe@someemail.com", + "jane doe;jadoe@someemail.com" + ], + "TargetedUserProtectionAction": "Quarantine" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Enabled_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "User impersonation protection SHOULD be enabled for key agency leaders" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : false, + "EnableTargetedUserProtection" : true, + "TargetedUsersToProtect" : [ + "john doe;jdoe@someemail.com", + "jane doe;jadoe@someemail.com" + ], + "TargetedUserProtectionAction" : "Quarantine" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No users are included for targeted user protection." +} + +test_EnableTargetedUserProtection_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "User impersonation protection SHOULD be enabled for key agency leaders" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : true, + "EnableTargetedUserProtection" : false, + "TargetedUsersToProtect" : [ + "john doe;jdoe@someemail.com", + "jane doe;jadoe@someemail.com" + ], + "TargetedUserProtectionAction" : "Quarantine" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No users are included for targeted user protection." +} + +test_TargetedUsersToProtect_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "User impersonation protection SHOULD be enabled for key agency leaders" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : true, + "EnableTargetedUserProtection" : true, + "TargetedUsersToProtect" : [ ], + "TargetedUserProtectionAction" : "Quarantine" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No users are included for targeted user protection." +} + +# +# Policy 2 +#-- +test_OrganizationDomain_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "Domain impersonation protection SHOULD be enabled for domains owned by the agency" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : true, + "EnableOrganizationDomainsProtection" : true, + "EnableTargetedDomainsProtection" : true, + "TargetedDomainProtectionAction" : "Quarantine", + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Enabled_Incorrect_V2 if { + ControlNumber := "Defender 2.5" + Requirement := "Domain impersonation protection SHOULD be enabled for domains owned by the agency" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : false, + "EnableOrganizationDomainsProtection" : true, + "EnableTargetedDomainsProtection" : true, + "TargetedDomainProtectionAction" : "Quarantine", + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableOrganizationDomainsProtection_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "Domain impersonation protection SHOULD be enabled for domains owned by the agency" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : true, + "EnableOrganizationDomainsProtection" : false, + "EnableTargetedDomainsProtection" : true, + "TargetedDomainProtectionAction" : "Quarantine", + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableTargetedDomainsProtection_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "Domain impersonation protection SHOULD be enabled for domains owned by the agency" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : true, + "EnableOrganizationDomainsProtection" : true, + "EnableTargetedDomainsProtection" : false, + "TargetedDomainProtectionAction" : "Quarantine", + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +# +# Policy 3 +#-- +test_CustomDomains_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "Domain impersonation protection SHOULD be added for frequent partners" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : true, + "EnableTargetedDomainsProtection" : true, + "TargetedDomainsToProtect" : [ "test domain" ], + "TargetedDomainProtectionAction" : "Quarantine" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Enabled_Incorrect_V3 if { + ControlNumber := "Defender 2.5" + Requirement := "Domain impersonation protection SHOULD be added for frequent partners" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : false, + "EnableTargetedDomainsProtection" : true, + "TargetedDomainsToProtect" : [ "test domain" ], + "TargetedDomainProtectionAction" : "Quarantine" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "The Custom Domains protection policies: Enabled, EnableTargetedDomainsProtection, and TargetedDomainsToProtect are not set correctly" +} + +test_EnableTargetedDomainsProtection_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "Domain impersonation protection SHOULD be added for frequent partners" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : true, + "EnableTargetedDomainsProtection" : false, + "TargetedDomainsToProtect" : [ "test domain" ], + "TargetedDomainProtectionAction" : "Quarantine" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "The Custom Domains protection policies: Enabled, EnableTargetedDomainsProtection, and TargetedDomainsToProtect are not set correctly" +} + +test_TargetedDomainsToProtect_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "Domain impersonation protection SHOULD be added for frequent partners" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : true, + "EnableTargetedDomainsProtection" : true, + "TargetedDomainsToProtect" : [ ], + "TargetedDomainProtectionAction" : "Quarantine" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "The Custom Domains protection policies: Enabled, EnableTargetedDomainsProtection, and TargetedDomainsToProtect are not set correctly" +} + +# +# Policy 4 +#-- +test_Email_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "Intelligence for impersonation protection SHALL be enabled" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : true, + "EnableMailboxIntelligenceProtection" : true, + "MailboxIntelligenceProtectionAction" : "Quarantine" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Enabled_Incorrect_V4 if { + ControlNumber := "Defender 2.5" + Requirement := "Intelligence for impersonation protection SHALL be enabled" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : false, + "EnableMailboxIntelligenceProtection" : true, + "MailboxIntelligenceProtectionAction" : "Quarantine" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableMailboxIntelligenceProtection_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "Intelligence for impersonation protection SHALL be enabled" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : true, + "EnableMailboxIntelligenceProtection" : false, + "MailboxIntelligenceProtectionAction" : "Quarantine" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +# +# Policy 5 +#-- +test_TargetedUserProtectionAction_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "Message action SHALL be set to quarantine if the message is detected as impersonated: users default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "TargetedUserProtectionAction" : "Quarantine", + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_TargetedUserProtectionAction_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "Message action SHALL be set to quarantine if the message is detected as impersonated: users default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "TargetedUserProtectionAction" : "Not Quarantine", + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_TargetedUserProtectionActionCustom_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "Message action SHOULD be set to quarantine if the message is detected as impersonated: users non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "TargetedUserProtectionAction" : "Quarantine", + "Identity" : "Custom 1" + }, + { + "TargetedUserProtectionAction" : "Not Quarantine", + "Identity" : "Office365 AntiPhish Default" # The default policy should be ignored here + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_TargetedUserProtectionActionCustom_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "Message action SHOULD be set to quarantine if the message is detected as impersonated: users non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "TargetedUserProtectionAction" : "Quarantine", + "Identity" : "Custom 1" + }, + { + "TargetedUserProtectionAction" : "Not Quarantine", + "Identity" : "Custom 2" + }, + { + "TargetedUserProtectionAction" : "Not Quarantine", + "Identity" : "Office365 AntiPhish Default" # The default policy should be ignored here + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 non-default anti phish policy(ies) found where the action for messages detected as user impersonation is not quarantine: Custom 2" +} + + +test_TargetedDomainProtectionAction_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "Message action SHALL be set to quarantine if the message is detected as impersonated: domains default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "TargetedDomainProtectionAction" : "Quarantine", + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_TargetedDomainProtectionAction_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "Message action SHALL be set to quarantine if the message is detected as impersonated: domains default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "TargetedDomainProtectionAction" : "Not Quarantine", + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_TargetedDomainProtectionActionCustom_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "Message action SHOULD be set to quarantine if the message is detected as impersonated: domains non-default policies" + Output := tests with input as { + "anti_phish_policies" : [ + { + "TargetedDomainProtectionAction" : "Quarantine", + "Identity" : "Custom 1" + }, + { + "TargetedDomainProtectionAction" : "Not Quarantine", + "Identity" : "Office365 AntiPhish Default" # The default policy should be ignored here + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_TargetedDomainProtectionActionCustom_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "Message action SHOULD be set to quarantine if the message is detected as impersonated: domains non-default policies" + Output := tests with input as { + "anti_phish_policies" : [ + { + "TargetedDomainProtectionAction" : "Quarantine", + "Identity" : "Custom 1" + }, + { + "TargetedDomainProtectionAction" : "Not Quarantine", + "Identity" : "Custom 2" + }, + { + "TargetedDomainProtectionAction" : "Not Quarantine", + "Identity" : "Custom 3" + }, + { + "TargetedDomainProtectionAction" : "Not Quarantine", + "Identity" : "Office365 AntiPhish Default" # The default policy should be ignored here + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + startswith(RuleOutput[0].ReportDetails, "2 non-default anti phish policy(ies) found where the action for messages detected as domain impersonation is not quarantine:") + # I don't think we can assume the order rego will Output these, hence the "includes" check instead of a simple == + contains(RuleOutput[0].ReportDetails, "Custom 2") + contains(RuleOutput[0].ReportDetails, "Custom 3") +} + +test_MailboxIntelligenceProtectionAction_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "Message action SHALL be set to quarantine if the message is detected as impersonated: mailbox default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "MailboxIntelligenceProtectionAction" : "Quarantine", + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_MailboxIntelligenceProtectionAction_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "Message action SHALL be set to quarantine if the message is detected as impersonated: mailbox default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "MailboxIntelligenceProtectionAction" : "Not Quarantine", + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_MailIntProtectionActionCustom_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "Message action SHOULD be set to quarantine if the message is detected as impersonated: mailbox non-default policies" + Output := tests with input as { + "anti_phish_policies" : [ + { + "MailboxIntelligenceProtectionAction" : "Quarantine", + "Identity" : "Custom 1" + }, + { + "MailboxIntelligenceProtectionAction" : "Quarantine", + "Identity" : "Custom 2" + }, + { + "MailboxIntelligenceProtectionAction" : "something else", + "Identity" : "Standard Preset Security Policy314195" # should be ignored + }, + { + "MailboxIntelligenceProtectionAction" : "Not Quarantine", + "Identity" : "Office365 AntiPhish Default" # The default policy should be ignored here + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails = "Requirement met" +} + +test_MailIntProtectionActionCustom_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "Message action SHOULD be set to quarantine if the message is detected as impersonated: mailbox non-default policies" + Output := tests with input as { + "anti_phish_policies" : [ + { + "MailboxIntelligenceProtectionAction" : "Quarantine", + "Identity" : "Custom 1" + }, + { + "MailboxIntelligenceProtectionAction" : "Not Quarantine", + "Identity" : "Custom 2" + }, + { + "MailboxIntelligenceProtectionAction" : "something else", + "Identity" : "Standard Preset Security Policy314195" # should be ignored + }, + { + "MailboxIntelligenceProtectionAction" : "Not Quarantine", + "Identity" : "Office365 AntiPhish Default" # The default policy should be ignored here + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails = "1 non-default anti phish policy(ies) found where the action for messages flagged by mailbox intelligence is not quarantine: Custom 2" +} + +# +# Policy 6 +#-- +test_AuthenticationFailAction_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "Mail classified as spoofed SHALL be quarantined: default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "AuthenticationFailAction" : "Quarantine", + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AuthenticationFailAction_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "Mail classified as spoofed SHALL be quarantined: default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "AuthenticationFailAction" : "Not Quarantine", + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_AuthenticationFailActionNonDefault_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "Mail classified as spoofed SHOULD be quarantined: non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "AuthenticationFailAction" : "Quarantine", + "Identity" : "Not Standard Preset SecurityPolicy1659535429826" + }, + { + "AuthenticationFailAction" : "Quarantine", + "Identity" : "Not Standard Preset SecurityPolicy Either" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AuthenticationFailActionNonDefault_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "Mail classified as spoofed SHOULD be quarantined: non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "AuthenticationFailAction" : "Not Quarantine", + "Identity" : "Not Standard Preset SecurityPolicy1659535429826" + }, + { + "AuthenticationFailAction" : "Quarantine", + "Identity" : "Not Standard Preset SecurityPolicy Either" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti phish policy(ies) found where the action for spoofed emails is not set to quarantine: Not Standard Preset SecurityPolicy1659535429826" + #Custom anti phish policy(ies) found where the action for spoofed emails is not set to quarantine. +} + +# +# Policy 7 +#-- +test_EnableFirstContactSafetyTipsDefault_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHALL be enabled: first contact default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableFirstContactSafetyTips" : true, + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableFirstContactSafetyTipsDefault_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHALL be enabled: first contact default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableFirstContactSafetyTips" : false, + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableFirstContactSafetyTipsNonDefault_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHOULD be enabled: first contact non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableFirstContactSafetyTips" : true, + "Identity" : "Custom policy 1" + }, + { + "EnableFirstContactSafetyTips" : true, + "Identity" : "Custom policy 2" + }, + { + "EnableFirstContactSafetyTips" : true, + "Identity" : "Custom policy 3" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableFirstContactSafetyTipsNonDefault_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHOULD be enabled: first contact non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableFirstContactSafetyTips" : true, + "Identity" : "Custom policy 1" + }, + { + "EnableFirstContactSafetyTips" : false, + "Identity" : "Custom policy 2" + }, + { + "EnableFirstContactSafetyTips" : true, + "Identity" : "Custom policy 3" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti phish policy(ies) found where first contact safety tips are not enabled: Custom policy 2" +} + +test_EnableSimilarUsersSafetyTipsDefault_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHALL be enabled: user impersonation default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableSimilarUsersSafetyTips" : true, + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableSimilarUsersSafetyTipsDefault_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHALL be enabled: user impersonation default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableSimilarUsersSafetyTips" : false, + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableSimilarUserSafetyTipsNonDefault_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHOULD be enabled: user impersonation non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableSimilarUsersSafetyTips" : true, + "Identity" : "Custom policy 1" + }, + { + "EnableSimilarUsersSafetyTips" : true, + "Identity" : "Custom policy 2" + }, + { + "EnableSimilarUsersSafetyTips" : true, + "Identity" : "Custom policy 3" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableSimilarUserSafetyTipsNonDefault_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHOULD be enabled: user impersonation non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableSimilarUsersSafetyTips" : true, + "Identity" : "Custom policy 1" + }, + { + "EnableSimilarUsersSafetyTips" : false, + "Identity" : "Custom policy 2" + }, + { + "EnableSimilarUsersSafetyTips" : true, + "Identity" : "Custom policy 3" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti phish policy(ies) found where similar user safety tips are not enabled: Custom policy 2" +} + +test_EnableSimilarDomainsSafetyTipsDomains_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHALL be enabled: domain impersonation default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableSimilarDomainsSafetyTips" : true, + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableSimilarDomainsSafetyTipsDefault_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHALL be enabled: domain impersonation default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableSimilarDomainsSafetyTips" : false, + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableSimilarDomainsSafetyTipsNonDefault_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHOULD be enabled: domain impersonation non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableSimilarDomainsSafetyTips" : true, + "Identity" : "Custom policy 1" + }, + { + "EnableSimilarDomainsSafetyTips" : true, + "Identity" : "Custom policy 2" + }, + { + "EnableSimilarDomainsSafetyTips" : true, + "Identity" : "Custom policy 3" + }, + { + "EnableSimilarDomainsSafetyTips" : false, # The default policy should be ignored + "Identity" : "Office365 AntiPhish Default" + }, + { + "EnableSimilarDomainsSafetyTips" : false, # The preset policy should be ignored too + "Identity" : "Standard Preset Security Policy12345" + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableSimilarDomainsSafetyTipsNonDefault_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHOULD be enabled: domain impersonation non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableSimilarDomainsSafetyTips" : true, + "Identity" : "Custom policy 1" + }, + { + "EnableSimilarDomainsSafetyTips" : false, + "Identity" : "Custom policy 2" + }, + { + "EnableSimilarDomainsSafetyTips" : true, + "Identity" : "Custom policy 3" + }, + { + "EnableSimilarDomainsSafetyTips" : false, # The default policy should be ignored + "Identity" : "Office365 AntiPhish Default" + }, + { + "EnableSimilarDomainsSafetyTips" : false, # The preset policy should be ignored too + "Identity" : "Standard Preset Security Policy12345" + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti phish policy(ies) found where similar domains safety tips are not enabled: Custom policy 2" +} + +test_EnableUnusualCharactersSafetyTips_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHALL be enabled: user impersonation unusual characters default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableUnusualCharactersSafetyTips" : true, + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableUnusualCharactersSafetyTips_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHALL be enabled: user impersonation unusual characters default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableUnusualCharactersSafetyTips" : false, + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableUnusualCharSafetyTipsNonDefault_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHOULD be enabled: user impersonation unusual characters non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableUnusualCharactersSafetyTips" : true, + "Identity" : "Custom policy 1" + }, + { + "EnableUnusualCharactersSafetyTips" : true, + "Identity" : "Custom policy 2" + }, + { + "EnableUnusualCharactersSafetyTips" : true, + "Identity" : "Custom policy 3" + }, + { + "EnableUnusualCharactersSafetyTips" : false, # The default policy should be ignored + "Identity" : "Office365 AntiPhish Default" + }, + { + "EnableUnusualCharactersSafetyTips" : false, # The preset policy should be ignored too + "Identity" : "Standard Preset Security Policy12345" + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableUnusualCharSafetyTipsNonDefault_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHOULD be enabled: user impersonation unusual characters non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableUnusualCharactersSafetyTips" : true, + "Identity" : "Custom policy 1" + }, + { + "EnableUnusualCharactersSafetyTips" : false, + "Identity" : "Custom policy 2" + }, + { + "EnableUnusualCharactersSafetyTips" : true, + "Identity" : "Custom policy 3" + }, + { + "EnableUnusualCharactersSafetyTips" : false, # The default policy should be ignored + "Identity" : "Office365 AntiPhish Default" + }, + { + "EnableUnusualCharactersSafetyTips" : false, # The preset policy should be ignored too + "Identity" : "Standard Preset Security Policy12345" + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti phish policy(ies) found where unusual character safety tips are not enabled: Custom policy 2" +} + +test_EnableViaTagDefault_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHALL be enabled: \"via\" tag default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableViaTag" : true, + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableViaTagDefault_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHALL be enabled: \"via\" tag default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableViaTag" : false, + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableViaTagSafetyTipsNonDefault_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHOULD be enabled: \"via\" tag non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableViaTag" : true, + "Identity" : "Custom policy 1" + }, + { + "EnableViaTag" : true, + "Identity" : "Custom policy 2" + }, + { + "EnableViaTag" : true, + "Identity" : "Custom policy 3" + }, + { + "EnableViaTag" : false, # The default policy should be ignored + "Identity" : "Office365 AntiPhish Default" + }, + { + "EnableViaTag" : false, # The preset policy should be ignored too + "Identity" : "Standard Preset Security Policy12345" + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableViaTagSafetyTipsNonDefault_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHOULD be enabled: \"via\" tag non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableViaTag" : true, + "Identity" : "Custom policy 1" + }, + { + "EnableViaTag" : false, + "Identity" : "Custom policy 2" + }, + { + "EnableViaTag" : true, + "Identity" : "Custom policy 3" + }, + { + "EnableViaTag" : false, # The default policy should be ignored + "Identity" : "Office365 AntiPhish Default" + }, + { + "EnableViaTag" : false, # The preset policy should be ignored too + "Identity" : "Standard Preset Security Policy12345" + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti phish policy(ies) found where via tag is not enabled: Custom policy 2" +} + +test_EnableUnauthenticatedSenderDefault_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHALL be enabled: \"?\" for unauthenticated senders for spoof default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableUnauthenticatedSender" : true, + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableUnauthenticatedSenderDefault_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHALL be enabled: \"?\" for unauthenticated senders for spoof default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableUnauthenticatedSender" : false, + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableUnauthSenderTipsNonDefault_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHOULD be enabled: \"?\" for unauthenticated senders for spoof non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableUnauthenticatedSender" : true, + "Identity" : "Custom policy 1" + }, + { + "EnableUnauthenticatedSender" : true, + "Identity" : "Custom policy 2" + }, + { + "EnableUnauthenticatedSender" : true, + "Identity" : "Custom policy 3" + }, + { + "EnableUnauthenticatedSender" : false, # The default policy should be ignored + "Identity" : "Office365 AntiPhish Default" + }, + { + "EnableUnauthenticatedSender" : false, # The preset policy should be ignored too + "Identity" : "Standard Preset Security Policy12345" + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableUnauthSenderTipsNonDefault_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHOULD be enabled: \"?\" for unauthenticated senders for spoof non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableUnauthenticatedSender" : true, + "Identity" : "Custom policy 1" + }, + { + "EnableUnauthenticatedSender" : false, + "Identity" : "Custom policy 2" + }, + { + "EnableUnauthenticatedSender" : true, + "Identity" : "Custom policy 3" + }, + { + "EnableUnauthenticatedSender" : false, # The default policy should be ignored + "Identity" : "Office365 AntiPhish Default" + }, + { + "EnableUnauthenticatedSender" : false, # The preset policy should be ignored too + "Identity" : "Standard Preset Security Policy12345" + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti phish policy(ies) found where '?' for unauthenticated sender is not enabled: Custom policy 2" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/Defender/DefenderConfig2_06_test.rego b/Testing/Unit/Rego/Defender/DefenderConfig2_06_test.rego new file mode 100644 index 0000000000..47394b771c --- /dev/null +++ b/Testing/Unit/Rego/Defender/DefenderConfig2_06_test.rego @@ -0,0 +1,1278 @@ +package defender +import future.keywords + + +# +# Policy 1 +#-- +test_BulkThreshold_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "The bulk complaint level (BCL) threshold SHOULD be set to six or lower: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "BulkThreshold": 6, + "Identity" : "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_BulkThreshold_CorrectV2 if { + ControlNumber := "Defender 2.6" + Requirement := "The bulk complaint level (BCL) threshold SHOULD be set to six or lower: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "BulkThreshold": 5, + "Identity" : "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_BulkThreshold_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "The bulk complaint level (BCL) threshold SHOULD be set to six or lower: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "BulkThreshold": 7, + "Identity" : "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +### + +test_NonDefBulkThreshold_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "The bulk complaint level (BCL) threshold SHOULD be set to six or lower: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "BulkThreshold": 6, + "Identity":"Not Default" + }, + { + "BulkThreshold": 6, + "Identity":"Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_NonDefBulkThreshold_CorrectV2 if { + ControlNumber := "Defender 2.6" + Requirement := "The bulk complaint level (BCL) threshold SHOULD be set to six or lower: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "BulkThreshold": 5, + "Identity":"Not Default" + }, + { + "BulkThreshold": 5, + "Identity":"Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_NonDefBulkThreshold_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "The bulk complaint level (BCL) threshold SHOULD be set to six or lower: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "BulkThreshold": 7, + "Identity":"Not Default" + }, + { + "BulkThreshold": 7, + "Identity":"Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti-spam policy(ies) found where bulk complaint level threshold is set to 7 or more: Not Default" +} + +# +# Policy 2 +#-- +test_SpamAction_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Spam SHALL be moved to either the junk email folder or the quarantine folder: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "SpamAction": "Quarantine", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_SpamAction_CorrectV2 if { + ControlNumber := "Defender 2.6" + Requirement := "Spam SHALL be moved to either the junk email folder or the quarantine folder: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "SpamAction": "MoveToJmf", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_SpamAction_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Spam SHALL be moved to either the junk email folder or the quarantine folder: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "SpamAction": "Not MoveToJmf", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_SpamAction_Incorrect_V2 if { + ControlNumber := "Defender 2.6" + Requirement := "Spam SHALL be moved to either the junk email folder or the quarantine folder: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "SpamAction": "Not Quarantine", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_HighConfSpamAction_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "High confidence spam SHALL be moved to either the junk email folder or the quarantine folder: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "HighConfidenceSpamAction": "Quarantine", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_HighConfSpamAction_CorrectV2 if { + ControlNumber := "Defender 2.6" + Requirement := "High confidence spam SHALL be moved to either the junk email folder or the quarantine folder: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "HighConfidenceSpamAction": "MoveToJmf", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_HighConfSpamAction_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "High confidence spam SHALL be moved to either the junk email folder or the quarantine folder: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "HighConfidenceSpamAction": "Not MoveToJmf", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_HighConfSpamAction_Incorrect_V2 if { + ControlNumber := "Defender 2.6" + Requirement := "High confidence spam SHALL be moved to either the junk email folder or the quarantine folder: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "HighConfidenceSpamAction": "Not Quarantine", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_Non_Def_SpamAction_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Spam SHOULD be moved to either the junk email folder or the quarantine folder: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "SpamAction": "Quarantine", + "Identity": "Not Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Non_Def_SpamAction_CorrectV2 if { + ControlNumber := "Defender 2.6" + Requirement := "Spam SHOULD be moved to either the junk email folder or the quarantine folder: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "SpamAction": "MoveToJmf", + "Identity": "Not Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Non_Def_SpamAction_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Spam SHOULD be moved to either the junk email folder or the quarantine folder: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "SpamAction": "Not MoveToJmf", + "Identity": "Not Default" + }, + { + "SpamAction": "Not MoveToJmf", + "Identity": "Default" + } ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti-spam policy(ies) found where spam is not being sent to the Quarantine folder or the Junk Mail Folder: Not Default" +} + +test_Non_Def_SpamAction_Incorrect_V2 if { + ControlNumber := "Defender 2.6" + Requirement := "Spam SHOULD be moved to either the junk email folder or the quarantine folder: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "SpamAction": "Not Quarantine", + "Identity": "Not Default" + }, + { + "SpamAction": "Not Quarantine", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti-spam policy(ies) found where spam is not being sent to the Quarantine folder or the Junk Mail Folder: Not Default" +} + +test_Non_Def_HighConfSpamAction_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "High confidence spam SHOULD be moved to either the junk email folder or the quarantine folder: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "HighConfidenceSpamAction": "Quarantine", + "Identity": "Not Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Non_Def_HighConfSpamAction_CorrectV2 if { + ControlNumber := "Defender 2.6" + Requirement := "High confidence spam SHOULD be moved to either the junk email folder or the quarantine folder: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "HighConfidenceSpamAction": "MoveToJmf", + "Identity": "Not Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Non_Def_HighConfSpamAction_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "High confidence spam SHOULD be moved to either the junk email folder or the quarantine folder: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "HighConfidenceSpamAction": "Not MoveToJmf", + "Identity": "Not Default" + }, + { + "HighConfidenceSpamAction": "Not MoveToJmf", + "Identity": "Default" + } ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti-spam policy(ies) found where high confidence spam is not being sent to the Quarantine folder or the Junk Mail Folder: Not Default"} + +test_Non_Def_HighConfSpamAction_Incorrect_V2 if { + ControlNumber := "Defender 2.6" + Requirement := "High confidence spam SHOULD be moved to either the junk email folder or the quarantine folder: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "HighConfidenceSpamAction": "Not Quarantine", + "Identity": "Not Default" + }, + { + "HighConfidenceSpamAction": "Not Quarantine", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti-spam policy(ies) found where high confidence spam is not being sent to the Quarantine folder or the Junk Mail Folder: Not Default"} + +# +# Policy 3 +#-- +test_PhishingSpamAction_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Phishing SHALL be quarantined: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "PhishSpamAction": "Quarantine", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_PhishingSpamAction_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Phishing SHALL be quarantined: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "PhishSpamAction": "Not Quarantine", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_HighConfidencePhishAction_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "High confidence phishing SHALL be quarantined: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "HighConfidencePhishAction": "Quarantine", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_HighConfidencePhishAction_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "High confidence phishing SHALL be quarantined: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "HighConfidencePhishAction": "Not Quarantine", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_Non_Def_PhishingSpamAction_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Phishing SHOULD be quarantined: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "PhishSpamAction": "Quarantine", + "Identity": "Not Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Non_Def_PhishingSpamAction_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Phishing SHOULD be quarantined: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "PhishSpamAction": "Not Quarantine", + "Identity": "Not Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti-spam policy(ies) found where phishing isn't moved to the quarantine folder: Not Default" +} + +test_Non_Def_HighConfidencePhishAction_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "High confidence phishing SHOULD be quarantined: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "HighConfidencePhishAction": "Quarantine", + "Identity": "Not Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Non_Def_HighConfidencePhishAction_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "High confidence phishing SHOULD be quarantined: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "HighConfidencePhishAction": "Not Quarantine", + "Identity": "Not Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti-spam policy(ies) found where high-confidence phishing isn't moved to quarantine folder: Not Default" +} + +# +# Policy 4 +#-- +test_BulkSpamAction_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Bulk email SHOULD be moved to either the junk email folder or the quarantine folder: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "BulkSpamAction": "Quarantine", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_BulkSpamAction_CorrectV2 if { + ControlNumber := "Defender 2.6" + Requirement := "Bulk email SHOULD be moved to either the junk email folder or the quarantine folder: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "BulkSpamAction": "MoveToJmf", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_BulkSpamAction_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Bulk email SHOULD be moved to either the junk email folder or the quarantine folder: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "BulkSpamAction": "Not Quarantine", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_BulkSpamAction_Incorrect_V2 if { + ControlNumber := "Defender 2.6" + Requirement := "Bulk email SHOULD be moved to either the junk email folder or the quarantine folder: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "BulkSpamAction": "Not MoveToJmf", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +# +# Policy 5 +#-- +test_QuarantineRetentionPeriod_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Spam in quarantine SHOULD be retained for at least 30 days: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "QuarantineRetentionPeriod": 30, + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_QuarantineRetentionPeriod_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Spam in quarantine SHOULD be retained for at least 30 days: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "QuarantineRetentionPeriod": 31, + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_QuarantineRetentionPeriod_Incorrect_V2 if { + ControlNumber := "Defender 2.6" + Requirement := "Spam in quarantine SHOULD be retained for at least 30 days: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "QuarantineRetentionPeriod": 29, + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_QuarantineRetentionPeriodCustom_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Spam in quarantine SHOULD be retained for at least 30 days: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "QuarantineRetentionPeriod": 30, + "Identity": "Custom 1" + }, + { + "QuarantineRetentionPeriod": 3, + "Identity": "Default" # This policy should be ignored + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_QuarantineRetentionPeriodCustom_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Spam in quarantine SHOULD be retained for at least 30 days: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "QuarantineRetentionPeriod": 30, + "Identity": "Custom 1" + }, + { + "QuarantineRetentionPeriod": 29, + "Identity": "Custom 2" + }, + { + "QuarantineRetentionPeriod": 3, + "Identity": "Default" # This policy should be ignored + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti-spam policy(ies) found where spam in quarantine isn't retained for 30 days: Custom 2" +} + +# +# Policy 6 +#-- +test_InlineSafetyTipsEnabled_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Spam safety tips SHOULD be turned on: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "InlineSafetyTipsEnabled": true, + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_InlineSafetyTipsEnabled_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Spam safety tips SHOULD be turned on: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "InlineSafetyTipsEnabled": false, + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_InlineSafetyTipsEnabledCustom_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Spam safety tips SHOULD be turned on: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "InlineSafetyTipsEnabled": false, + "Identity": "Default" # should be ignored + }, + { + "InlineSafetyTipsEnabled": true, + "Identity": "Custom 1" + }, + { + "InlineSafetyTipsEnabled": true, + "Identity": "Custom 2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_InlineSafetyTipsEnabledCustom_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Spam safety tips SHOULD be turned on: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "InlineSafetyTipsEnabled": false, + "Identity": "Default" # should be ignored + }, + { + "InlineSafetyTipsEnabled": false, + "Identity": "Custom 1" + }, + { + "InlineSafetyTipsEnabled": true, + "Identity": "Custom 2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti-spam policy(ies) found where spam safety tips is disabled: Custom 1" +} + +# +# Policy 7 +#-- +test_ZapEnabled_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Zero-hour auto purge (ZAP) SHALL be enabled: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "ZapEnabled": true, + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ZapEnabled_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Zero-hour auto purge (ZAP) SHALL be enabled: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "ZapEnabled": false, + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_SpamZapEnabled_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Zero-hour auto purge (ZAP) SHALL be enabled for spam messages: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "SpamZapEnabled": true, + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_SpamZapEnabled_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Zero-hour auto purge (ZAP) SHALL be enabled for spam messages: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "SpamZapEnabled": false, + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_PhishZapEnabled_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Zero-hour auto purge (ZAP) SHALL be enabled for phishing: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "PhishZapEnabled": true, + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_PhishZapEnabled_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Zero-hour auto purge (ZAP) SHALL be enabled for phishing: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "PhishZapEnabled": false, + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_ZapEnabledCustom_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Zero-hour auto purge (ZAP) SHOULD be enabled: non-default" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "ZapEnabled": true, + "Identity": "Not Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ZapEnabledCustom_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Zero-hour auto purge (ZAP) SHOULD be enabled: non-default" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "ZapEnabled": false, + "Identity": "Not Default" + }, + { + "ZapEnabled": false, + "Identity": "Default" # SHOULD be ignored + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti-spam policies found where Zero-hour auto purge is disabled: Not Default" +} + +test_SpamZapEnabled_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Zero-hour auto purge (ZAP) SHOULD be enabled for Spam: non-default" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "SpamZapEnabled": true, + "Identity": "Not Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_SpamZapEnabledCustom_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Zero-hour auto purge (ZAP) SHOULD be enabled for Spam: non-default" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "SpamZapEnabled": false, + "Identity": "Not Default" + }, + { + "SpamZapEnabled": false, # should be ignored + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti-spam policies found where Zero-hour auto purge for spam is disabled: Not Default" +} + +test_PhishZapEnabledCustom_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Zero-hour auto purge (ZAP) SHOULD be enabled for phishing: non-default" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "PhishZapEnabled": true, + "Identity": "Not Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_PhishZapEnabledCustom_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Zero-hour auto purge (ZAP) SHOULD be enabled for phishing: non-default" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "PhishZapEnabled": false, + "Identity": "Not Default" + }, + { + "PhishZapEnabled": false, # should be ignored + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti-spam policy(ies) found where Zero-hour auto purge for phishing is disabled: Not Default" +} + +# +# Policy 8 +#-- +test_AllowedSenderDomainsNotEmpty_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Allowed senders MAY be added but allowed domains SHALL NOT be added: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "AllowedSenderDomains": [], + "Identity": "Default" + } + ] + } + + + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowedSenderDomainsNotEmpty_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Allowed senders MAY be added but allowed domains SHALL NOT be added: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "AllowedSenderDomains": [""], + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti-spam policy(ies) found where there is at least one allowed sender domain: Default" +} + +test_AllowedSenderDomainsNotEmptyCustom_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Allowed senders MAY be added but allowed domains SHOULD NOT be added: non-default" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "AllowedSenderDomains": [], + "Identity": "Not Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowedSenderDomainsNotEmptyCustom_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Allowed senders MAY be added but allowed domains SHOULD NOT be added: non-default" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "AllowedSenderDomains": [""], + "Identity": "Not Default" + }, + { + "AllowedSenderDomains": [""], + "Identity": "Default" + } + ] + } + + + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + print(RuleOutput) + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom policy(ies) found where there is at least one allowed sender domain: Not Default" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/Defender/DefenderConfig2_07_test.rego b/Testing/Unit/Rego/Defender/DefenderConfig2_07_test.rego new file mode 100644 index 0000000000..c92e2e4399 --- /dev/null +++ b/Testing/Unit/Rego/Defender/DefenderConfig2_07_test.rego @@ -0,0 +1,1412 @@ +package defender +import future.keywords + + +# +# Policy 1 +#-- +test_Domains_Correct if { + ControlNumber := "Defender 2.7" + Requirement := "The Safe Links Policy SHALL include all agency domains-and by extension-all users" + + Output := tests with input as { + "safe_links_rules" : [ + { + "RecipientDomainIs": ["Test Domain"], + "Identity" : "Test Policy Rule", + "SafeLinksPolicy": "Test Policy", + "State" : "Enabled" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_DomainName_Incorrect_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "The Safe Links Policy SHALL include all agency domains-and by extension-all users" + + Output := tests with input as { + "safe_links_rules" : [ + { + "RecipientDomainIs": [""], + "Identity" : "Test Policy", + "SafeLinksPolicy": "Test Policy", + "State" : "Enabled" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No policy found that applies to all domains: Test Domain" +} + +test_DomainName_Incorrect_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "The Safe Links Policy SHALL include all agency domains-and by extension-all users" + + Output := tests with input as { + "safe_links_rules" : [ + { + "RecipientDomainIs": ["Test Domain2"], + "Identity" : "Test Policy", + "SafeLinksPolicy": "Test Policy", + "State" : "Enabled" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No policy found that applies to all domains: Test Domain" +} + +test_Domains_Incorrect_v3 if { + ControlNumber := "Defender 2.7" + Requirement := "The Safe Links Policy SHALL include all agency domains-and by extension-all users" + + Output := tests with input as { + "safe_links_rules" : [ + { + "RecipientDomainIs": ["Test Domain"], + "Identity" : "Test Policy", + "SafeLinksPolicy": "Test Policy", + "State" : "Disabled" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No policy found that applies to all domains: Test Domain" +} + +test_Domains_Incorrect_V4 if { + # If no defender license is present, the provider will output "safe_links_rules" + # and "safe_links_policies" as empty lists + ControlNumber := "Defender 2.7" + Requirement := "The Safe Links Policy SHALL include all agency domains-and by extension-all users" + + Output := tests with input as { + "safe_links_rules": [], + "safe_links_policies": [], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "defender_license" : false + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met **NOTE: Either you do not have sufficient permissions or your tenant does not have a license for Microsoft Defender for Office 365 Plan 1, which is required for this feature.**" +} + +# +# Policy 2 +#-- +test_EnableSafeLinksForEmail_Correct_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "URL rewriting and malicious link click checking SHALL be enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Enabled", + "SafeLinksPolicy": "a", + "Identity" : "a" + }, + { + "State" : "Enabled", + "SafeLinksPolicy": "b", + "Identity" : "b rule" + } + ], + "safe_links_policies": [ + { + "EnableSafeLinksForEmail" : true, + "Identity": "a" + }, + { + "EnableSafeLinksForEmail" : false, + "Identity": "b" + }, + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableSafeLinksForEmail_Correct_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "URL rewriting and malicious link click checking SHALL be enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Enabled", + "SafeLinksPolicy": "b", + "Identity" : "a" + }, + { + "State" : "Disabled", + "SafeLinksPolicy": "b", + "Identity" : "b rule" + } + ], + "safe_links_policies": [ + { + "EnableSafeLinksForEmail" : true, + "Identity": "a" + }, + { + "EnableSafeLinksForEmail" : true, + "Identity": "b" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableSafeLinksForEmail_Incorrect_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "URL rewriting and malicious link click checking SHALL be enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "Identity" : "Default Rule", + "SafeLinksPolicy": "Default", + "State" : "Enabled" + } + ], + "safe_links_policies": [ + { + "EnableSafeLinksForEmail" : false, + "Identity": "Default" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableSafeLinksForEmail_Incorrect_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "URL rewriting and malicious link click checking SHALL be enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "Identity" : "Default Rule", + "SafeLinksPolicy": "Default", + "State" : "Disabled" + } + ], + "safe_links_policies": [ + { + "EnableSafeLinksForEmail" : true, + "Identity": "Default" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableSafeLinksForEmail_Incorrect_V3 if { + # If no defender license is present, the provider will output "safe_links_rules" + # and "safe_links_policies" as empty lists + ControlNumber := "Defender 2.7" + Requirement := "URL rewriting and malicious link click checking SHALL be enabled" + + Output := tests with input as { + "safe_links_rules" : [], + "safe_links_policies" : [], + "defender_license" : false + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met **NOTE: Either you do not have sufficient permissions or your tenant does not have a license for Microsoft Defender for Office 365 Plan 1, which is required for this feature.**" +} + +# +# Policy 3 +#-- +test_EnableSafeLinksForTeams_Correct_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "Malicious link click checking SHALL be enabled with Microsoft Teams" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Enabled", + "SafeLinksPolicy": "a", + "Identity" : "a rule" + }, + { + "State" : "Enabled", + "SafeLinksPolicy": "b", + "Identity" : "a" + } + ], + "safe_links_policies": [ + { + "EnableSafeLinksForTeams" : true, + "Identity": "a" + }, + { + "EnableSafeLinksForTeams" : false, + "Identity": "b" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableSafeLinksForTeams_Correct_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "Malicious link click checking SHALL be enabled with Microsoft Teams" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Disabled", + "Identity" : "a", + "SafeLinksPolicy": "a" + }, + { + "State" : "Enabled", + "Identity" : "b rule", + "SafeLinksPolicy": "b" + } + ], + "safe_links_policies": [ + { + "EnableSafeLinksForTeams" : true, + "Identity": "a" + }, + { + "EnableSafeLinksForTeams" : true, + "Identity": "b" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableSafeLinksForTeams_Incorrect_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "Malicious link click checking SHALL be enabled with Microsoft Teams" + + Output := tests with input as { + "safe_links_rules": [ + { + "Identity" : "Default", + "SafeLinksPolicy": "Default", + "State" : "Enabled" + } + ], + "safe_links_policies": [ + { + "EnableSafeLinksForTeams" : false, + "Identity": "Default" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableSafeLinksForTeams_Incorrect_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "Malicious link click checking SHALL be enabled with Microsoft Teams" + + Output := tests with input as { + "safe_links_rules": [ + { + "Identity" : "Default", + "SafeLinksPolicy": "Default", + "State" : "Disabled" + } + ], + "safe_links_policies": [ + { + "EnableSafeLinksForTeams" : false, + "Identity": "Default" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableSafeLinksForTeams_Incorrect_V3 if { + # If no defender license is present, the provider will output "safe_links_rules" + # and "safe_links_policies" as empty lists + ControlNumber := "Defender 2.7" + Requirement := "Malicious link click checking SHALL be enabled with Microsoft Teams" + + Output := tests with input as { + "safe_links_rules" : [], + "safe_links_policies" : [], + "defender_license" : false + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met **NOTE: Either you do not have sufficient permissions or your tenant does not have a license for Microsoft Defender for Office 365 Plan 1, which is required for this feature.**" +} + +# +# Policy 4 +#-- +test_ScanUrls_Correct_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "Real-time suspicious URL and file-link scanning SHALL be enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Enabled", + "SafeLinksPolicy": "a", + "Identity" : "a" + }, + { + "State" : "Enabled", + "SafeLinksPolicy": "b", + "Identity" : "b" + } + ], + "safe_links_policies": [ + { + "ScanUrls" : false, + "Identity": "a" + }, + { + "ScanUrls" : true, + "Identity": "b" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ScanUrls_Correct_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "Real-time suspicious URL and file-link scanning SHALL be enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Disabled", + "SafeLinksPolicy": "a", + "Identity" : "a" + }, + { + "State" : "Enabled", + "SafeLinksPolicy": "b", + "Identity" : "b" + } + ], + "safe_links_policies": [ + { + "ScanUrls" : true, + "Identity": "a" + }, + { + "ScanUrls" : true, + "Identity": "b" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ScanUrls_Incorrect_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "Real-time suspicious URL and file-link scanning SHALL be enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "Identity" : "Default", + "SafeLinksPolicy": "Default", + "State" : "Enabled" + } + ], + "safe_links_policies": [ + { + "ScanUrls" : false, + "Identity": "Default" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_ScanUrls_Incorrect_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "Real-time suspicious URL and file-link scanning SHALL be enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "Identity" : "Default", + "SafeLinksPolicy": "Default", + "State" : "Disabled" + } + ], + "safe_links_policies": [ + { + "ScanUrls" : false, + "Identity": "Default" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_ScanUrls_Incorrect_V3 if { + # If no defender license is present, the provider will output "safe_links_rules" + # and "safe_links_policies" as empty lists + ControlNumber := "Defender 2.7" + Requirement := "Real-time suspicious URL and file-link scanning SHALL be enabled" + + Output := tests with input as { + "safe_links_rules" : [], + "safe_links_policies" : [], + "defender_license" : false + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met **NOTE: Either you do not have sufficient permissions or your tenant does not have a license for Microsoft Defender for Office 365 Plan 1, which is required for this feature.**" +} + +# +# Policy 5 +#-- +test_DeliverMessageAfterScan_Correct_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "URLs SHALL be scanned completely before message delivery" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Enabled", + "SafeLinksPolicy": "a", + "Identity" : "a rule" + }, + { + "State" : "Enabled", + "SafeLinksPolicy": "b", + "Identity" : "b" + } + ], + "safe_links_policies": [ + { + "DeliverMessageAfterScan" : true, + "Identity": "a" + }, + { + "DeliverMessageAfterScan" : false, + "Identity": "b" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_DeliverMessageAfterScan_Correct_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "URLs SHALL be scanned completely before message delivery" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Enabled", + "SafeLinksPolicy": "a", + "Identity" : "a" + }, + { + "State" : "Disabled", + "SafeLinksPolicy": "b", + "Identity" : "b rule" + } + ], + "safe_links_policies": [ + { + "DeliverMessageAfterScan" : true, + "Identity": "a" + }, + { + "DeliverMessageAfterScan" : true, + "Identity": "b" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_DeliverMessageAfterScan_Incorrect_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "URLs SHALL be scanned completely before message delivery" + + Output := tests with input as { + "safe_links_rules": [ + { + "Identity" : "Default", + "SafeLinksPolicy": "Default", + "State" : "Enabled" + } + ], + "safe_links_policies": [ + { + "DeliverMessageAfterScan" : false, + "Identity": "Default" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_DeliverMessageAfterScan_Incorrect_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "URLs SHALL be scanned completely before message delivery" + + Output := tests with input as { + "safe_links_rules": [ + { + "Identity" : "Default", + "SafeLinksPolicy": "Default", + "State" : "Disabled" + } + ], + "safe_links_policies": [ + { + "DeliverMessageAfterScan" : false, + "Identity": "Default" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_DeliverMessageAfterScan_Incorrect_V3 if { + # If no defender license is present, the provider will output "safe_links_rules" + # and "safe_links_policies" as empty lists + ControlNumber := "Defender 2.7" + Requirement := "URLs SHALL be scanned completely before message delivery" + + Output := tests with input as { + "safe_links_rules" : [], + "safe_links_policies" : [], + "defender_license" : false + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met **NOTE: Either you do not have sufficient permissions or your tenant does not have a license for Microsoft Defender for Office 365 Plan 1, which is required for this feature.**" +} + +# +# Policy 6 +#-- +test_EnableForInternalSenders_Correct_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "Internal agency email messages SHALL have safe links enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Enabled", + "SafeLinksPolicy": "a", + "Identity" : "a" + }, + { + "State" : "Enabled", + "SafeLinksPolicy": "b", + "Identity" : "b" + } + ], + "safe_links_policies": [ + { + "Identity" : "a", + "EnableForInternalSenders" : false + }, + { + "Identity" : "b", + "EnableForInternalSenders" : true + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableForInternalSenders_Correct_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "Internal agency email messages SHALL have safe links enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Disabled", + "SafeLinksPolicy": "a", + "Identity" : "a" + }, + { + "State" : "Enabled", + "SafeLinksPolicy": "b", + "Identity" : "b rule" + } + ], + "safe_links_policies": [ + { + "Identity" : "a", + "EnableForInternalSenders" : true + }, + { + "Identity" : "b", + "EnableForInternalSenders" : true + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableForInternalSenders_Incorrect_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "Internal agency email messages SHALL have safe links enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "Identity" : "Default", + "SafeLinksPolicy": "Default", + "State" : "Enabled" + } + ], + "safe_links_policies": [ + { + "Identity" : "Not Built-in Protection Policy", + "EnableForInternalSenders" : false + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableForInternalSenders_Incorrect_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "Internal agency email messages SHALL have safe links enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "Identity" : "Default", + "SafeLinksPolicy": "Default", + "State" : "Disabled" + } + ], + "safe_links_policies": [ + { + "Identity" : "Not Built-in Protection Policy", + "EnableForInternalSenders" : false + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableForInternalSenders_Incorrect_V3 if { + # If no defender license is present, the provider will output "safe_links_rules" + # and "safe_links_policies" as empty lists + ControlNumber := "Defender 2.7" + Requirement := "Internal agency email messages SHALL have safe links enabled" + + Output := tests with input as { + "safe_links_rules" : [], + "safe_links_policies" : [], + "defender_license" : false + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met **NOTE: Either you do not have sufficient permissions or your tenant does not have a license for Microsoft Defender for Office 365 Plan 1, which is required for this feature.**" +} + +# +# Policy 7 +#-- +test_TrackClicks_Correct_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "User click tracking SHALL be enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Enabled", + "SafeLinksPolicy": "a", + "Identity" : "a" + }, + { + "State" : "Enabled", + "SafeLinksPolicy": "b", + "Identity" : "b" + } + ], + "safe_links_policies": [ + { + "TrackClicks" : false, + "Identity": "a" + }, + { + "TrackClicks" : true, + "Identity": "b" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_TrackClicks_Correct_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "User click tracking SHALL be enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Enabled", + "SafeLinksPolicy": "a", + "Identity" : "a" + }, + { + "State" : "Disabled", + "SafeLinksPolicy": "b", + "Identity" : "b" + } + ], + "safe_links_policies": [ + { + "TrackClicks" : true, + "Identity": "a" + }, + { + "TrackClicks" : true, + "Identity": "b" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_TrackClicks_Incorrect_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "User click tracking SHALL be enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "Identity" : "Default", + "State" : "Enabled", + "SafeLinksPolicy": "Default" + } + ], + "safe_links_policies": [ + { + "TrackClicks" : false, + "Identity": "Default" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_TrackClicks_Incorrect_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "User click tracking SHALL be enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "Identity" : "Default", + "State" : "Disabled", + "SafeLinksPolicy": "Default" + } + ], + "safe_links_policies": [ + { + "TrackClicks" : false, + "Identity": "Default" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_TrackClicks_Incorrect_V3 if { + # If no defender license is present, the provider will output "safe_links_rules" + # and "safe_links_policies" as empty lists + ControlNumber := "Defender 2.7" + Requirement := "User click tracking SHALL be enabled" + + Output := tests with input as { + "safe_links_rules" : [], + "safe_links_policies" : [], + "defender_license" : false + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met **NOTE: Either you do not have sufficient permissions or your tenant does not have a license for Microsoft Defender for Office 365 Plan 1, which is required for this feature.**" +} + +# +# Policy 8 +#-- +test_EnableSafeLinksForOffice_Correct_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "Safe Links in Office 365 apps SHALL be turned on" + + Output := tests with input as { + "safe_links_rules": [ + { + "Identity" : "a rule", + "SafeLinksPolicy": "a", + "State" : "Enabled" + }, + { + "Identity" : "b", + "SafeLinksPolicy": "b", + "State" : "Enabled" + } + ], + "safe_links_policies": [ + { + "EnableSafeLinksForOffice" : true, + "Identity" : "a" + }, + { + # Only one policy needs to be true. + "EnableSafeLinksForOffice" : false, + "Identity" : "b" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableSafeLinksForOffice_Correct_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "Safe Links in Office 365 apps SHALL be turned on" + + Output := tests with input as { + "safe_links_rules": [ + { + "Identity" : "a rule", + "SafeLinksPolicy": "a", + "State" : "Enabled" + }, + { + # Only one policy needs to be true. + "Identity" : "b", + "SafeLinksPolicy": "b", + "State" : "Disabled" + } + ], + "safe_links_policies": [ + { + "EnableSafeLinksForOffice" : true, + "Identity" : "a" + }, + { + "EnableSafeLinksForOffice" : true, + "Identity" : "b" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableSafeLinksForOffice_Incorrect_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "Safe Links in Office 365 apps SHALL be turned on" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Disabled", + "SafeLinksPolicy": "a", + "Identity" : "a" + } + ], + "safe_links_policies": [ + { + "EnableSafeLinksForOffice" : false, + "Identity" : "a" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableSafeLinksForOffice_Incorrect_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "Safe Links in Office 365 apps SHALL be turned on" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Disabled", + "SafeLinksPolicy": "a", + "Identity" : "a" + } + ], + "safe_links_policies": [ + { + "EnableSafeLinksForOffice" : true, + "Identity" : "a" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableSafeLinksForOffice_Incorrect_V3 if { + ControlNumber := "Defender 2.7" + Requirement := "Safe Links in Office 365 apps SHALL be turned on" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Enabled", + "SafeLinksPolicy": "a", + "Identity" : "a" + } + ], + "safe_links_policies": [ + { + "EnableSafeLinksForOffice" : false, + "Identity" : "a" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableSafeLinksForOffice_Incorrect_V4 if { + # If no defender license is present, the provider will output "safe_links_rules" + # and "safe_links_policies" as empty lists + ControlNumber := "Defender 2.7" + Requirement := "Safe Links in Office 365 apps SHALL be turned on" + + Output := tests with input as { + "safe_links_rules" : [], + "safe_links_policies" : [], + "defender_license" : false + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met **NOTE: Either you do not have sufficient permissions or your tenant does not have a license for Microsoft Defender for Office 365 Plan 1, which is required for this feature.**" +} + +# +# Policy 9 +#-- +test_AllowClickThrough_Correct_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "Users SHALL NOT be enabled to click through to the original URL" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Enabled", + "SafeLinksPolicy": "a", + "Identity" : "a rule" + }, + { + "State" : "Enabled", + "SafeLinksPolicy": "b", + "Identity" : "b" + } + ], + "safe_links_policies": [ + { + "AllowClickThrough" : false, + "Identity" : "a" + }, + { + "AllowClickThrough" : true, + "Identity" : "b" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowClickThrough_Correct_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "Users SHALL NOT be enabled to click through to the original URL" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Enabled", + "SafeLinksPolicy": "a", + "Identity" : "a rule" + }, + { + "State" : "Disabled", + "SafeLinksPolicy": "b", + "Identity" : "b" + } + ], + "safe_links_policies": [ + { + "AllowClickThrough" : false, + "Identity" : "a" + }, + { + "AllowClickThrough" : false, + "Identity" : "b" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowClickThrough_Incorrect_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "Users SHALL NOT be enabled to click through to the original URL" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Disabled", + "SafeLinksPolicy": "a", + "Identity" : "a" + } + ], + "safe_links_policies": [ + { + "AllowClickThrough" : true, + "Identity" : "a" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_AllowClickThrough_Incorrect_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "Users SHALL NOT be enabled to click through to the original URL" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Disabled", + "SafeLinksPolicy": "a", + "Identity" : "a" + } + ], + "safe_links_policies": [ + { + "AllowClickThrough" : false, + "Identity" : "a" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_AllowClickThrough_Incorrect_V3 if { + ControlNumber := "Defender 2.7" + Requirement := "Users SHALL NOT be enabled to click through to the original URL" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Enabled", + "SafeLinksPolicy": "a", + "Identity" : "a" + } + ], + "safe_links_policies": [ + { + "AllowClickThrough" : true, + "Identity" : "a" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_AllowClickThrough_Incorrect_V4 if { + # If no defender license is present, the provider will output "safe_links_rules" + # and "safe_links_policies" as empty lists + ControlNumber := "Defender 2.7" + Requirement := "Users SHALL NOT be enabled to click through to the original URL" + + Output := tests with input as { + "safe_links_rules" : [], + "safe_links_policies" : [], + "defender_license" : false + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met **NOTE: Either you do not have sufficient permissions or your tenant does not have a license for Microsoft Defender for Office 365 Plan 1, which is required for this feature.**" +} diff --git a/Testing/Unit/Rego/Defender/DefenderConfig2_08_test.rego b/Testing/Unit/Rego/Defender/DefenderConfig2_08_test.rego new file mode 100644 index 0000000000..237657a8eb --- /dev/null +++ b/Testing/Unit/Rego/Defender/DefenderConfig2_08_test.rego @@ -0,0 +1,466 @@ +package defender +import future.keywords + + +# +# Policy 1 +#-- +test_Domains_Correct if { + ControlNumber := "Defender 2.8" + Requirement := "At least one Safe Attachments Policy SHALL include all agency domains-and by extension-all users" + + Output := tests with input as { + "safe_attachment_rules" : [ + { + "RecipientDomainIs": ["Test Domain"], + "SafeAttachmentPolicy": "Test Policy", + "Identity" : "Test Policy Rule" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_DomainName_Incorrect_V1 if { + ControlNumber := "Defender 2.8" + Requirement := "At least one Safe Attachments Policy SHALL include all agency domains-and by extension-all users" + + Output := tests with input as { + "safe_attachment_rules" : [ + { + "RecipientDomainIs": [""], + "SafeAttachmentPolicy": "Test Policy", + "Identity" : "Test Policy Rule" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No policy found that applies to all domains: Test Domain" +} + +test_DomainName_Incorrect_V2 if { + ControlNumber := "Defender 2.8" + Requirement := "At least one Safe Attachments Policy SHALL include all agency domains-and by extension-all users" + + Output := tests with input as { + "safe_attachment_rules" : [ + { + "RecipientDomainIs": ["Test Domain2"], + "SafeAttachmentPolicy": "Test Policy", + "Identity" : "Test Policy Rule" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No policy found that applies to all domains: Test Domain" +} + +test_DomainName_Incorrect_V3 if { + # If no defender license is present, the provider will output "safe_attachment_rules" + # and "safe_attachment_policies" as empty lists + ControlNumber := "Defender 2.8" + Requirement := "At least one Safe Attachments Policy SHALL include all agency domains-and by extension-all users" + + Output := tests with input as { + "safe_attachment_rules" : [], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "safe_attachment_policies" : [], + "defender_license" : false + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met **NOTE: Either you do not have sufficient permissions or your tenant does not have a license for Microsoft Defender for Office 365 Plan 1, which is required for this feature.**" +} + +# +# Policy 2 +#-- +test_Action_Correct if { + ControlNumber := "Defender 2.8" + Requirement := "The action for malware in email attachments SHALL be set to block" + + Output := tests with input as { + "safe_attachment_rules": [ + { + "RecipientDomainIs": ["Test Domain"], + "SafeAttachmentPolicy": "Test Policy", + "Identity" : "Test Policy Rule" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "safe_attachment_policies" : [ + { + "Action" : "Block", + "Enable" : true, + "RedirectAddress" : "127.0.0.1", + "Identity" : "Test Policy" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_RecipientDomainIs_Incorrect_V1 if { + ControlNumber := "Defender 2.8" + Requirement := "The action for malware in email attachments SHALL be set to block" + + Output := tests with input as { + "safe_attachment_rules": [ + { + "RecipientDomainIs": [""], + "SafeAttachmentPolicy": "Test Policy", + "Identity" : "Test Policy" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "safe_attachment_policies" : [ + { + "Action" : "Block", + "Enable" : true, + "RedirectAddress" : "127.0.0.1", + "Identity" : "Test Policy" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No enabled policy found with action set to block that apply to all domains" +} + +test_RecipientDomainIs_Incorrect_V2 if { + ControlNumber := "Defender 2.8" + Requirement := "The action for malware in email attachments SHALL be set to block" + + Output := tests with input as { + "safe_attachment_rules": [ + { + "RecipientDomainIs": ["Test Domain2"], + "SafeAttachmentPolicy": "Test Policy", + "Identity" : "Test Policy" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "safe_attachment_policies" : [ + { + "Action" : "Block", + "Enable" : true, + "RedirectAddress" : "127.0.0.1", + "Identity" : "Test Policy" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No enabled policy found with action set to block that apply to all domains" +} + +test_Action_Incorrect_V1 if { + ControlNumber := "Defender 2.8" + Requirement := "The action for malware in email attachments SHALL be set to block" + + Output := tests with input as { + "safe_attachment_rules": [ + { + "RecipientDomainIs": ["Test Domain"], + "SafeAttachmentPolicy": "Test Policy", + "Identity" : "Test Policy" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "safe_attachment_policies" : [ + { + "Action" : "Not Block", + "Enable" : true, + "RedirectAddress" : "127.0.0.1", + "Identity" : "Test Policy" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No enabled policy found with action set to block that apply to all domains" +} + +test_Action_Incorrect_V2 if { + # If no defender license is present, the provider will output "safe_attachment_rules" + # and "safe_attachment_policies" as empty lists + ControlNumber := "Defender 2.8" + Requirement := "The action for malware in email attachments SHALL be set to block" + + Output := tests with input as { + "safe_attachment_rules" : [], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "safe_attachment_policies" : [], + "defender_license" : false + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met **NOTE: Either you do not have sufficient permissions or your tenant does not have a license for Microsoft Defender for Office 365 Plan 1, which is required for this feature.**" +} + +test_Enable_Incorrect if { + ControlNumber := "Defender 2.8" + Requirement := "The action for malware in email attachments SHALL be set to block" + + Output := tests with input as { + "safe_attachment_rules": [ + { + "RecipientDomainIs": ["Test Domain"], + "SafeAttachmentPolicy": "Test Policy", + "Identity" : "Test Policy " + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "safe_attachment_policies" : [ + { + "Action" : "Block", + "Enable" : false, + "RedirectAddress" : "127.0.0.1", + "Identity" : "Test Policy" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No enabled policy found with action set to block that apply to all domains" +} + +test_Identity_Incorrect if { + ControlNumber := "Defender 2.8" + Requirement := "The action for malware in email attachments SHALL be set to block" + + Output := tests with input as { + "safe_attachment_rules": [ + { + "RecipientDomainIs": ["Test Domain"], + "SafeAttachmentPolicy": "Test Policy", + "Identity" : "Test Policy" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "safe_attachment_policies" : [ + { + "Action" : "Block", + "Enable" : true, + "RedirectAddress" : "127.0.0.1", + "Identity" : "" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No enabled policy found with action set to block that apply to all domains" +} + +# +# Policy 3 +#-- +test_RedirectPolicies_Correct if { + ControlNumber := "Defender 2.8" + Requirement := "Redirect emails with detected attachments to an agency-specified email SHOULD be enabled" + + Output := tests with input as { + "safe_attachment_rules": [ + { + "RecipientDomainIs": ["Test Domain"], + "SafeAttachmentPolicy": "Test Policy", + "Identity" : "Test Policy Rule" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "safe_attachment_policies" : [ + { + "Action" : "Block", + "Enable" : true, + "RedirectAddress" : "127.0.0.1", + "Identity" : "Test Policy" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_RedirectAddress_Incorrect if { + ControlNumber := "Defender 2.8" + Requirement := "Redirect emails with detected attachments to an agency-specified email SHOULD be enabled" + + Output := tests with input as { + "safe_attachment_rules": [ + { + "RecipientDomainIs": ["Test Domain"], + "SafeAttachmentPolicy": "Test Policy", + "Identity" : "Test Policy Rule" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "safe_attachment_policies" : [ + { + "Action" : "Block", + "Enable" : true, + "RedirectAddress" : "", + "Identity" : "Test Policy" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No enabled policy found with action set to block and at least one contact specified" +} + +# +# Policy 4 +#-- +test_Spot_Correct if { + ControlNumber := "Defender 2.8" + Requirement := "Safe attachments SHOULD be enabled for SharePoint, OneDrive, and Microsoft Teams" + + Output := tests with input as { + "atp_policy_for_o365" : [ + { + "EnableATPForSPOTeamsODB" : true, + "Identity" : "Default" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Spot_Incorrect if { + ControlNumber := "Defender 2.8" + Requirement := "Safe attachments SHOULD be enabled for SharePoint, OneDrive, and Microsoft Teams" + + Output := tests with input as { + "atp_policy_for_o365" : [ + { + "EnableATPForSPOTeamsODB" : false, + "Identity" : "Default" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} diff --git a/Testing/Unit/Rego/Defender/DefenderConfig2_09_test.rego b/Testing/Unit/Rego/Defender/DefenderConfig2_09_test.rego new file mode 100644 index 0000000000..0fa1e3e6ca --- /dev/null +++ b/Testing/Unit/Rego/Defender/DefenderConfig2_09_test.rego @@ -0,0 +1,233 @@ +package defender +import future.keywords + + +# +# Policy 1 +#-- +test_Disabled_Correct if { + ControlNumber := "Defender 2.9" + Requirement := "At a minimum, the alerts required by the Exchange Online Minimum Viable Secure Configuration Baseline SHALL be enabled" + + Output := tests with input as { + "protection_alerts": [ + { + "Name": "Suspicious email sending patterns detected", + "Disabled": false + }, + { + "Name": "Unusual increase in email reported as phish", + "Disabled": false + }, + { + "Name": "Suspicious Email Forwarding Activity", + "Disabled": false + }, + { + "Name": "Messages have been delayed", + "Disabled": false + }, + { + "Name": "Tenant restricted from sending unprovisioned email", + "Disabled": false + }, + { + "Name": "User restricted from sending email", + "Disabled": false + }, + { + "Name": "Malware campaign detected after delivery", + "Disabled": false + }, + { + "Name": "A potentially malicious URL click was detected", + "Disabled": false + }, + { + "Name": "Suspicious connector activity", + "Disabled": false + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Disabled_Correct_V2 if { + # Test having extra alerts enabled that aren't required by the baseline + # SHOULDn't matter + ControlNumber := "Defender 2.9" + Requirement := "At a minimum, the alerts required by the Exchange Online Minimum Viable Secure Configuration Baseline SHALL be enabled" + + Output := tests with input as { + "protection_alerts": [ + { + "Name": "Suspicious email sending patterns detected", + "Disabled": false + }, + { + "Name": "Unusual increase in email reported as phish", + "Disabled": false + }, + { + "Name": "Suspicious Email Forwarding Activity", + "Disabled": false + }, + { + "Name": "Messages have been delayed", + "Disabled": false + }, + { + "Name": "Tenant restricted from sending unprovisioned email", + "Disabled": false + }, + { + "Name": "User restricted from sending email", + "Disabled": false + }, + { + "Name": "Malware campaign detected after delivery", + "Disabled": false + }, + { + "Name": "A potentially malicious URL click was detected", + "Disabled": false + }, + { + "Name": "Suspicious connector activity", + "Disabled": false + }, + { + "Name": "Successful exact data match upload", # Not required + "Disabled": false + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Disabled_Incorrect if { + ControlNumber := "Defender 2.9" + Requirement := "At a minimum, the alerts required by the Exchange Online Minimum Viable Secure Configuration Baseline SHALL be enabled" + + Output := tests with input as { + "protection_alerts": [ + { + "Name": "Suspicious email sending patterns detected", + "Disabled": true # SHOULD be false + }, + { + "Name": "Unusual increase in email reported as phish", + "Disabled": false + }, + { + "Name": "Suspicious Email Forwarding Activity", + "Disabled": false + }, + { + "Name": "Messages have been delayed", + "Disabled": false + }, + { + "Name": "Tenant restricted from sending unprovisioned email", + "Disabled": false + }, + { + "Name": "User restricted from sending email", + "Disabled": false + }, + { + "Name": "Malware campaign detected after delivery", + "Disabled": false + }, + { + "Name": "A potentially malicious URL click was detected", + "Disabled": false + }, + { + "Name": "Suspicious connector activity", + "Disabled": false + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 disabled required alert(s) found: Suspicious email sending patterns detected" +} + +test_Disabled_Incorrect_V2 if { + # What happens if the alert is entirely missing instead of just disabled? + ControlNumber := "Defender 2.9" + Requirement := "At a minimum, the alerts required by the Exchange Online Minimum Viable Secure Configuration Baseline SHALL be enabled" + + Output := tests with input as { + "protection_alerts": [ + { + "Name": "Unusual increase in email reported as phish", + "Disabled": false + }, + { + "Name": "Suspicious Email Forwarding Activity", + "Disabled": false + }, + { + "Name": "Messages have been delayed", + "Disabled": false + }, + { + "Name": "Tenant restricted from sending unprovisioned email", + "Disabled": false + }, + { + "Name": "User restricted from sending email", + "Disabled": false + }, + { + "Name": "Malware campaign detected after delivery", + "Disabled": false + }, + { + "Name": "A potentially malicious URL click was detected", + "Disabled": false + }, + { + "Name": "Suspicious connector activity", + "Disabled": false + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 disabled required alert(s) found: Suspicious email sending patterns detected" +} + +# +# Policy 2 +#-- +test_NotImplemented_Correct if { + ControlNumber := "Defender 2.9" + Requirement := "The alerts SHOULD be sent to a monitored address or incorporated into a SIEM" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Defender Secure Configuration Baseline policy 2.9 for instructions on manual check" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/Defender/DefenderConfig2_10_test.rego b/Testing/Unit/Rego/Defender/DefenderConfig2_10_test.rego new file mode 100644 index 0000000000..d156d109cf --- /dev/null +++ b/Testing/Unit/Rego/Defender/DefenderConfig2_10_test.rego @@ -0,0 +1,74 @@ +package defender +import future.keywords + + +# +# Policy 1 +#-- +test_AdminAuditLogEnabled_Correct if { + ControlNumber := "Defender 2.10" + Requirement := "Unified audit logging SHALL be enabled" + + Output := tests with input as { + "admin_audit_log_config": [{ + "Identity": "Admin Audit Log Settings", + "UnifiedAuditLogIngestionEnabled" : true + }] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AdminAuditLogEnabled_Incorrect if { + ControlNumber := "Defender 2.10" + Requirement := "Unified audit logging SHALL be enabled" + + Output := tests with input as { + "admin_audit_log_config": [{ + "Identity": "Admin Audit Log Settings", + "UnifiedAuditLogIngestionEnabled" : false + }] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +# +# Policy 2 +#-- +test_NotImplemented_Correct_V1 if { + ControlNumber := "Defender 2.10" + Requirement := "Advanced audit SHALL be enabled" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Defender Secure Configuration Baseline policy 2.10 for instructions on manual check" +} + +# +# Policy 3 +#-- +test_NotImplemented_Correct_V2 if { + ControlNumber := "Defender 2.10" + Requirement := "Audit logs SHALL be maintained for at least the minimum duration dictated by OMB M-21-31" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Defender Secure Configuration Baseline policy 2.10 for instructions on manual check" +} diff --git a/Testing/Unit/Rego/EXO/EXOConfig2_01_test.rego b/Testing/Unit/Rego/EXO/EXOConfig2_01_test.rego new file mode 100644 index 0000000000..cb699a6ea4 --- /dev/null +++ b/Testing/Unit/Rego/EXO/EXOConfig2_01_test.rego @@ -0,0 +1,102 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_AutoForwardEnabled_Correct if { + ControlNumber := "EXO 2.1" + Requirement := "Automatic forwarding to external domains SHALL be disabled" + + Output := tests with input as { + "remote_domains": [ + { + "AutoForwardEnabled" : false, + "DomainName" : "Test name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AutoForwardEnabled_Incorrect_V1 if { + ControlNumber := "EXO 2.1" + Requirement := "Automatic forwarding to external domains SHALL be disabled" + + Output := tests with input as { + "remote_domains": [ + { + "AutoForwardEnabled" : true, + "DomainName" : "Test name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 remote domain(s) that allows automatic forwarding: Test name" +} + +test_AutoForwardEnabled_Incorrect_V2 if { + ControlNumber := "EXO 2.1" + Requirement := "Automatic forwarding to external domains SHALL be disabled" + + Output := tests with input as { + "remote_domains": [ + { + "AutoForwardEnabled" : true, + "DomainName" : "Test name" + }, + { + "AutoForwardEnabled" : true, + "DomainName" : "Test name 2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "2 remote domain(s) that allows automatic forwarding: Test name, Test name 2" +} + +test_AutoForwardEnabled_Incorrect_V3 if { + ControlNumber := "EXO 2.1" + Requirement := "Automatic forwarding to external domains SHALL be disabled" + + Output := tests with input as { + "remote_domains": [ + { + "AutoForwardEnabled" : true, + "DomainName" : "Test name" + }, + { + "AutoForwardEnabled" : true, + "DomainName" : "Test name 2" + }, + { + "AutoForwardEnabled" : false, + "DomainName" : "Test name 3" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "2 remote domain(s) that allows automatic forwarding: Test name, Test name 2" +} + +# TODO: what about the case where "remote_domains" is empty? +# Is this possible? Or will the default domain "*" always be there? +# Requires exploration online and manual testing. \ No newline at end of file diff --git a/Testing/Unit/Rego/EXO/EXOConfig2_02_test.rego b/Testing/Unit/Rego/EXO/EXOConfig2_02_test.rego new file mode 100644 index 0000000000..a6a414d735 --- /dev/null +++ b/Testing/Unit/Rego/EXO/EXOConfig2_02_test.rego @@ -0,0 +1,197 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_NotImplemented_Correct if { + ControlNumber := "EXO 2.2" + Requirement := "A list of approved IP addresses for sending mail SHALL be maintained" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Exchange Online Secure Configuration Baseline policy 2.# for instructions on manual check" +} + +# +# Policy 2 +#-- +test_Rdata_Correct if { + ControlNumber := "EXO 2.2" + Requirement := "An SPF policy(s) that designates only these addresses as approved senders SHALL be published" + + Output := tests with input as { + "spf_records": [ + { + "rdata" : ["v=spf1 "], + "domain" : "Test name" + } + ] + } + + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Rdata_Correct_V2 if { + ControlNumber := "EXO 2.2" + Requirement := "An SPF policy(s) that designates only these addresses as approved senders SHALL be published" + + Output := tests with input as { + "spf_records": [ + { + "rdata" : ["v=spf1 something"], + "domain" : "Test name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Rdata_Incorrect if { + ControlNumber := "EXO 2.2" + Requirement := "An SPF policy(s) that designates only these addresses as approved senders SHALL be published" + + Output := tests with input as { + "spf_records": [ + { + "rdata" : ["spf1 "], + "domain" : "Test name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 1 agency domain(s) found in violation: Test name" +} + +test_Rdata_Incorrect_V2 if { + ControlNumber := "EXO 2.2" + Requirement := "An SPF policy(s) that designates only these addresses as approved senders SHALL be published" + + Output := tests with input as { + "spf_records": [ + { + "rdata" : [""], + "domain" : "Test name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 1 agency domain(s) found in violation: Test name" +} + +test_Rdata_Incorrect_V3 if { + ControlNumber := "EXO 2.2" + Requirement := "An SPF policy(s) that designates only these addresses as approved senders SHALL be published" + + Output := tests with input as { + "spf_records": [ + { + "rdata" : ["v=spf1 "], + "domain" : "good.com" + }, + { + "rdata" : [""], + "domain" : "bad.com" + }, + { + "rdata" : [""], + "domain" : "2bad.com" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + contains(RuleOutput[0].ReportDetails, "2 of 3 agency domain(s) found in violation: ") + startswith(RuleOutput[0].ReportDetails, "2 of 3 agency domain(s) found in violation: ") + contains(RuleOutput[0].ReportDetails, "bad.com") # I'm not sure + + # if we can make any assumptions about the order these domains + # will be printed in, hence the "contains" operator instead of == + contains(RuleOutput[0].ReportDetails, "2bad.com") +} + +test_Rdata_Multiple_Correct_V1 if { + ControlNumber := "EXO 2.2" + Requirement := "An SPF policy(s) that designates only these addresses as approved senders SHALL be published" + + Output := tests with input as { + "spf_records": [ + { + "rdata" : ["v=spf1 ", "extra stuff that shouldn't matter"], + "domain" : "good.com" + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Rdata_Multiple_Correct_V2 if { + ControlNumber := "EXO 2.2" + Requirement := "An SPF policy(s) that designates only these addresses as approved senders SHALL be published" + + Output := tests with input as { + "spf_records": [ + { + "rdata" : ["extra stuff that shouldn't matter", "v=spf1 "], + "domain" : "good.com" + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Rdata_Multiple_Incorrect if { + ControlNumber := "EXO 2.2" + Requirement := "An SPF policy(s) that designates only these addresses as approved senders SHALL be published" + + Output := tests with input as { + "spf_records": [ + { + "rdata" : ["extra stuff that shouldn't matter", "hello world"], + "domain" : "bad.com" + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 1 agency domain(s) found in violation: bad.com" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/EXO/EXOConfig2_03_test.rego b/Testing/Unit/Rego/EXO/EXOConfig2_03_test.rego new file mode 100644 index 0000000000..4ae9e0e0a7 --- /dev/null +++ b/Testing/Unit/Rego/EXO/EXOConfig2_03_test.rego @@ -0,0 +1,347 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_Enabled_Correct_V1 if { + ControlNumber := "EXO 2.3" + Requirement := "DKIM SHOULD be enabled for any custom domain" + + Output := tests with input as { + "dkim_config": [ + { + "Enabled" : true, + "Domain" : "test.name" + } + ], + "dkim_records": [ + { + "rdata" : "v=DKIM1;", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Enabled_Correct_V2 if { + # Test with incorrect default domain + ControlNumber := "EXO 2.3" + Requirement := "DKIM SHOULD be enabled for any custom domain" + + Output := tests with input as { + "dkim_config": [ + { + "Enabled" : true, + "Domain" : "test.name" + }, + { + "Enabled" : false, + "Domain" : "example.onmicrosoft.com" # The baseline policy + # doesn't apply to the default domains, so this should be + # ignored. + } + ], + "dkim_records": [ + { + "rdata" : "v=DKIM1;", + "domain" : "test.name" + }, + { + "rdata" : "", + "domain" : "example.onmicrosoft.com" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + }, + { + "rdata" : "spf1 ", + "domain" : "example.onmicrosoft.com" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Enabled_Correct_V3 if { + # Test for multiple custom domains + ControlNumber := "EXO 2.3" + Requirement := "DKIM SHOULD be enabled for any custom domain" + + Output := tests with input as { + "dkim_config": [ + { + "Enabled" : true, + "Domain" : "test.name" + }, + { + "Enabled" : true, + "Domain" : "test2.name" + } + ], + "dkim_records": [ + { + "rdata" : "v=DKIM1;", + "domain" : "test.name" + }, + { + "rdata" : "v=DKIM1;", + "domain" : "test2.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + }, + { + "rdata" : "spf1 ", + "domain" : "test2.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Enabled_Correct_V4 if { + # Test for no custom domains, just the default domain + ControlNumber := "EXO 2.3" + Requirement := "DKIM SHOULD be enabled for any custom domain" + + Output := tests with input as { + "dkim_config": [ + { + "Enabled" : true, + "Domain" : "example.onmicrosoft.com" + } + ], + "dkim_records": [ + { + "rdata" : "v=DKIM1;", + "domain" : "example.onmicrosoft.com" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "example.onmicrosoft.com" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Enabled_Incorrect if { + ControlNumber := "EXO 2.3" + Requirement := "DKIM SHOULD be enabled for any custom domain" + + Output := tests with input as { + "dkim_config": [ + { + "Enabled" : false, + "Domain" : "test.name" + } + ], + "dkim_records": [ + { + "rdata" : "v=DKIM1;", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 1 agency domain(s) found in violation: test.name" +} + +test_Rdata_Incorrect if { + ControlNumber := "EXO 2.3" + Requirement := "DKIM SHOULD be enabled for any custom domain" + + Output := tests with input as { + "dkim_config": [ + { + "Enabled" : true, + "Domain" : "test.name" + } + ], + "dkim_records": [ + { + "rdata" : " ", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 1 agency domain(s) found in violation: test.name" +} + +test_Rdata_Incorrect_V2 if { + ControlNumber := "EXO 2.3" + Requirement := "DKIM SHOULD be enabled for any custom domain" + + Output := tests with input as { + "dkim_config": [ + { + "Enabled" : true, + "Domain" : "test.name" + } + ], + "dkim_records": [ + { + "rdata" : "Hello World", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 1 agency domain(s) found in violation: test.name" +} + +test_Enabled_Correct_V2 if { + ControlNumber := "EXO 2.3" + Requirement := "DKIM SHOULD be enabled for any custom domain" + + Output := tests with input as { + "dkim_config": [ + { + "Enabled" : true, + "Domain" : "test.name" + }, + { + "Enabled" : true, + "Domain" : "test2.name" + } + ], + "dkim_records": [ + { + "rdata" : "v=DKIM1;", + "domain" : "test.name" + }, + { + "rdata" : "v=DKIM1;", + "domain" : "test2.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + }, + { + "rdata" : "spf1 ", + "domain" : "test2.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Enabled_Inorrect_V3 if { + ControlNumber := "EXO 2.3" + Requirement := "DKIM SHOULD be enabled for any custom domain" + + Output := tests with input as { + "dkim_config": [ + { + "Enabled" : true, + "Domain" : "test.name" + }, + { + "Enabled" : false, + "Domain" : "test2.name" + } + ], + "dkim_records": [ + { + "rdata" : "v=DKIM1;", + "domain" : "test.name" + }, + { + "rdata" : "v=DKIM1;", + "domain" : "test2.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + }, + { + "rdata" : "spf1 ", + "domain" : "test2.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 2 agency domain(s) found in violation: test2.name" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/EXO/EXOConfig2_04_test.rego b/Testing/Unit/Rego/EXO/EXOConfig2_04_test.rego new file mode 100644 index 0000000000..19002ed66d --- /dev/null +++ b/Testing/Unit/Rego/EXO/EXOConfig2_04_test.rego @@ -0,0 +1,335 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_Rdata_Correct if { + ControlNumber := "EXO 2.4" + Requirement := "A DMARC policy SHALL be published for every second-level domain" + + Output := tests with input as { + "dmarc_records":[ + { + "rdata" : "v=DMARC1; p=reject; pct=100; rua=mailto:DMARC@hq.dhs.gov, mailto:reports@dmarc.cyber.dhs.gov", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Rdata_Incorrect if { + ControlNumber := "EXO 2.4" + Requirement := "A DMARC policy SHALL be published for every second-level domain" + + Output := tests with input as { + "dmarc_records":[ + { + "rdata" : " ", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 1 agency domain(s) found in violation: test.name" +} + +test_Rdata_Incorrect_V2 if { + ControlNumber := "EXO 2.4" + Requirement := "A DMARC policy SHALL be published for every second-level domain" + + Output := tests with input as { + "dmarc_records":[ + { + "rdata" : "v=DMARC1", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 1 agency domain(s) found in violation: test.name" +} + +test_Rdata_Incorrect_V3 if { + ControlNumber := "EXO 2.4" + Requirement := "A DMARC policy SHALL be published for every second-level domain" + + Output := tests with input as { + "dmarc_records": [ + { + "rdata" : "v=DMARC1; p=reject; pct=100; rua=mailto:DMARC@hq.dhs.gov, mailto:reports@dmarc.cyber.dhs.gov", + "domain" : "test.name" + }, + { + "rdata" : "", + "domain" : "bad.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + }, + { + "rdata" : "spf1 ", + "domain" : "bad.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 2 agency domain(s) found in violation: bad.name" +} + +# +# Policy 2 +#-- +test_Rdata_Correct_V2 if { + ControlNumber := "EXO 2.4" + Requirement := "The DMARC message rejection option SHALL be \"p=reject\"" + + Output := tests with input as { + "dmarc_records": [ + { + "rdata" : "v=DMARC1; p=reject; pct=100; rua=mailto:DMARC@hq.dhs.gov, mailto:reports@dmarc.cyber.dhs.gov", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Rdata_Incorrect_V4 if { + ControlNumber := "EXO 2.4" + Requirement := "The DMARC message rejection option SHALL be \"p=reject\"" + + Output := tests with input as { + "dmarc_records": [ + { + "rdata" : "v=DMARC1; p=none; mailto:reports@dmarc.cyber.dhs.gov mailto:jsmith@dhs.gov mailto:jsomething@dhs.gov", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 1 agency domain(s) found in violation: test.name" +} + +test_Rdata_Incorrect_V5 if { + ControlNumber := "EXO 2.4" + Requirement := "The DMARC message rejection option SHALL be \"p=reject\"" + + Output := tests with input as { + "dmarc_records": [ + { + "rdata" : "v=DMARC1; mailto:reports@dmarc.cyber.dhs.gov mailto:jsmith@dhs.gov mailto:jsomething@dhs.gov", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 1 agency domain(s) found in violation: test.name" +} + +# +# Policy 3 +#-- +test_Rdata_Correct_V3 if { + ControlNumber := "EXO 2.4" + Requirement := "The DMARC point of contact for aggregate reports SHALL include reports@dmarc.cyber.dhs.gov" + + Output := tests with input as { + "dmarc_records": [ + { + "rdata" : "v=DMARC1; p=reject; pct=100; rua=mailto:DMARC@hq.dhs.gov, mailto:reports@dmarc.cyber.dhs.gov", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Rdata_Incorrect_V6 if { + ControlNumber := "EXO 2.4" + Requirement := "The DMARC point of contact for aggregate reports SHALL include reports@dmarc.cyber.dhs.gov" + + Output := tests with input as { + "dmarc_records": [ + { + "rdata" : "v=DMARC1; p=reject; pct=100;", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 1 agency domain(s) found in violation: test.name" +} + +test_Rdata_Incorrect_V7 if { + ControlNumber := "EXO 2.4" + Requirement := "The DMARC point of contact for aggregate reports SHALL include reports@dmarc.cyber.dhs.gov" + + Output := tests with input as { + "dmarc_records": [ + { + "rdata" : "v=DMARC1; p=reject; pct=100; rua=mailto:reports@wrong.address", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 1 agency domain(s) found in violation: test.name" +} + +# +# Policy 4 +#-- +test_Rdata_Incorrect_V4 if { + ControlNumber := "EXO 2.4" + Requirement := "An agency point of contact SHOULD be included for aggregate and/or failure reports" + + Output := tests with input as { + "dmarc_records": [ + { + "rdata" : "v=DMARC1; p=reject; pct=100; rua=mailto:DMARC@hq.dhs.gov, mailto:reports@dmarc.cyber.dhs.gov", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Rdata_Incorrect_V8 if { + ControlNumber := "EXO 2.4" + Requirement := "An agency point of contact SHOULD be included for aggregate and/or failure reports" + + Output := tests with input as { + "dmarc_records": [ + { + "rdata" : "v=DMARC1; p=reject; pct=100; rua=mailto:reports@dmarc.cyber.dhs.gov", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 1 agency domain(s) found in violation: test.name" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/EXO/EXOConfig2_05_test.rego b/Testing/Unit/Rego/EXO/EXOConfig2_05_test.rego new file mode 100644 index 0000000000..d8e5983b5f --- /dev/null +++ b/Testing/Unit/Rego/EXO/EXOConfig2_05_test.rego @@ -0,0 +1,47 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_SmtpClientAuthenticationDisabled_Correct if { + ControlNumber := "EXO 2.5" + Requirement := "SMTP AUTH SHALL be disabled in Exchange Online" + + Output := tests with input as { + "transport_config": + [ + { + "SmtpClientAuthenticationDisabled" : true, + "Name":"A" + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_SmtpClientAuthenticationDisabled_Incorrect if { + ControlNumber := "EXO 2.5" + Requirement := "SMTP AUTH SHALL be disabled in Exchange Online" + + Output := tests with input as { + "transport_config": [ + { + "SmtpClientAuthenticationDisabled" : false, + "Name" : "A" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/EXO/EXOConfig2_06_test.rego b/Testing/Unit/Rego/EXO/EXOConfig2_06_test.rego new file mode 100644 index 0000000000..2c71008367 --- /dev/null +++ b/Testing/Unit/Rego/EXO/EXOConfig2_06_test.rego @@ -0,0 +1,106 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_Domains_Contacts_Correct if { + ControlNumber := "EXO 2.6" + Requirement := "Contact folders SHALL NOT be shared with all domains, although they MAY be shared with specific domains" + + Output := tests with input as { + "sharing_policy": [ + { + "Domains" : [ + "domain1", + "domain2" + ], + "Name":"A" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Domains_Contacts_Incorrect if { + ControlNumber := "EXO 2.6" + Requirement := "Contact folders SHALL NOT be shared with all domains, although they MAY be shared with specific domains" + + Output := tests with input as { + "sharing_policy": [ + { + "Domains" : [ + "*", + "domain1" + ], + "Name": "A" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Wildcard domain (\"*\") in shared domains list, enabling sharing with all domains by default" + + # print(count(RuleOutput)==1) + # notror := RuleOutput[0].RequirementMet + # trace(notror) + # print(RuleOutput[0].ReportDetails == "Wildcard domain (\"*\") in shared domains list, enabling sharing will all domains by default") +} + +# +# Policy 2 +#-- +test_Domains_Calender_Correct if { + ControlNumber := "EXO 2.6" + Requirement := "Calendar details SHALL NOT be shared with all domains, although they MAY be shared with specific domains" + + Output := tests with input as { + "sharing_policy": [ + { + "Domains" : [ + "domain1", + "domain2" + ], + "Name":"A" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Domains_Calender_Incorrect if { + ControlNumber := "EXO 2.6" + Requirement := "Calendar details SHALL NOT be shared with all domains, although they MAY be shared with specific domains" + + Output := tests with input as { + "sharing_policy": [ + { + "Domains" : [ + "*", + "domain1" + ], + "Name": "A" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Wildcard domain (\"*\") in shared domains list, enabling sharing with all domains by default" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/EXO/EXOConfig2_07_test.rego b/Testing/Unit/Rego/EXO/EXOConfig2_07_test.rego new file mode 100644 index 0000000000..893676ed91 --- /dev/null +++ b/Testing/Unit/Rego/EXO/EXOConfig2_07_test.rego @@ -0,0 +1,88 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_FromScope_Correct if { + ControlNumber := "EXO 2.7" + Requirement := "External sender warnings SHALL be implemented" + + Output := tests with input as { + "transport_rule": [ + { + "FromScope" : "NotInOrganization" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_FromScope_Incorrect if { + ControlNumber := "EXO 2.7" + Requirement := "External sender warnings SHALL be implemented" + + Output := tests with input as { + "transport_rule": [ + { + "FromScope" : "" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No transport rule found with that applies to emails received from outside the organization" +} + +test_FromScope_Multiple_Correct if { + ControlNumber := "EXO 2.7" + Requirement := "External sender warnings SHALL be implemented" + + Output := tests with input as { + "transport_rule": [ + { + "FromScope" : "" + }, + { + "FromScope" : "NotInOrganization" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_FromScope_Multiple_Incorrect if { + ControlNumber := "EXO 2.7" + Requirement := "External sender warnings SHALL be implemented" + + Output := tests with input as { + "transport_rule": [ + { + "FromScope" : "" + }, + { + "FromScope" : "Hello there" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No transport rule found with that applies to emails received from outside the organization" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/EXO/EXOConfig2_08_test.rego b/Testing/Unit/Rego/EXO/EXOConfig2_08_test.rego new file mode 100644 index 0000000000..81423b9d84 --- /dev/null +++ b/Testing/Unit/Rego/EXO/EXOConfig2_08_test.rego @@ -0,0 +1,35 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_3rdParty_Correct_V1 if { + ControlNumber := "EXO 2.8" + Requirement := "A DLP solution SHALL be used. The selected DLP solution SHOULD offer services comparable to the native DLP solution offered by Microsoft" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} + +# +# Policy 2 +#-- +test_3rdParty_Correct_V2 if { + ControlNumber := "EXO 2.8" + Requirement := "The DLP solution SHALL protect PII and sensitive information, as defined by the agency. At a minimum, the sharing of credit card numbers, Taxpayer Identification Numbers (TIN), and Social Security Numbers (SSN) via email SHALL be restricted" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/EXO/EXOConfig2_09_test.rego b/Testing/Unit/Rego/EXO/EXOConfig2_09_test.rego new file mode 100644 index 0000000000..43b6257e9a --- /dev/null +++ b/Testing/Unit/Rego/EXO/EXOConfig2_09_test.rego @@ -0,0 +1,51 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_3rdParty_Correct_V1 if { + ControlNumber := "EXO 2.9" + Requirement := "Emails SHALL be filtered by the file types of included attachments. The selected filtering solution SHOULD offer services comparable to Microsoft Defender's Common Attachment Filter" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} + +# +# Policy 2 +#-- +test_3rdParty_Correct_V2 if { + ControlNumber := "EXO 2.9" + Requirement := "The attachment filter SHOULD attempt to determine the true file type and assess the file extension" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} + +# +# Policy 3 +#-- +test_3rdParty_Correct_V3 if { + ControlNumber := "EXO 2.9" + Requirement := "Disallowed file types SHALL be determined and set. At a minimum, click-to-run files SHOULD be blocked (e.g., .exe, .cmd, and .vbe)" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/EXO/EXOConfig2_10_test.rego b/Testing/Unit/Rego/EXO/EXOConfig2_10_test.rego new file mode 100644 index 0000000000..6e42ce7d24 --- /dev/null +++ b/Testing/Unit/Rego/EXO/EXOConfig2_10_test.rego @@ -0,0 +1,51 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_3rdParty_Correct_V1 if { + ControlNumber := "EXO 2.10" + Requirement := "Emails SHALL be scanned for malware" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} + +# +# Policy 2 +#-- +test_3rdParty_Correct_V2 if { + ControlNumber := "EXO 2.10" + Requirement := "Emails identified as containing malware SHALL be quarantined or dropped" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} + +# +# Policy 3 +#-- +test_3rdParty_Correct_V3 if { + ControlNumber := "EXO 2.10" + Requirement := "Email scanning SHOULD be capable of reviewing emails after delivery" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/EXO/EXOConfig2_11_test.rego b/Testing/Unit/Rego/EXO/EXOConfig2_11_test.rego new file mode 100644 index 0000000000..3d1fca13ed --- /dev/null +++ b/Testing/Unit/Rego/EXO/EXOConfig2_11_test.rego @@ -0,0 +1,51 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_3rdParty_Correct_V1 if { + ControlNumber := "EXO 2.11" + Requirement := "Impersonation protection checks SHOULD be used" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} + +# +# Policy 2 +#-- +test_3rdParty_Correct_V2 if { + ControlNumber := "EXO 2.11" + Requirement := "User warnings, comparable to the user safety tips included with EOP, SHOULD be displayed" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} + +# +# Policy 3 +#-- +test_3rdParty_Correct_V3 if { + ControlNumber := "EXO 2.11" + Requirement := "The phishing protection solution SHOULD include an AI-based phishing detection tool comparable to EOP Mailbox Intelligence" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/EXO/EXOConfig2_12_test.rego b/Testing/Unit/Rego/EXO/EXOConfig2_12_test.rego new file mode 100644 index 0000000000..6d9f06ff16 --- /dev/null +++ b/Testing/Unit/Rego/EXO/EXOConfig2_12_test.rego @@ -0,0 +1,136 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_IPAllowList_Correct_V1 if { + ControlNumber := "EXO 2.12" + Requirement := "IP allow lists SHOULD NOT be created" + + Output := tests with input as { + "conn_filter": [ + { + "IPAllowList" : [], + "EnableSafeList": false, + "Name":"A" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_IPAllowList_Correct_V2 if { + ControlNumber := "EXO 2.12" + Requirement := "IP allow lists SHOULD NOT be created" + + Output := tests with input as { + "conn_filter": + [ + { + "IPAllowList" : [], + "EnableSafeList": true, + "Name":"A" + } # it shouldn't matter that safe list is enabled + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_IPAllowList_Incorrect if { + ControlNumber := "EXO 2.12" + Requirement := "IP allow lists SHOULD NOT be created" + + Output := tests with input as { + "conn_filter": [ + { + "IPAllowList" : ["trust.me.please"], + "EnableSafeList": false, + "Name" : "A" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Allow-list is in use" +} + +# +# Policy 2 +#-- +test_EnableSafeList_Correct_V1 if { + ControlNumber := "EXO 2.12" + Requirement := "Safe lists SHOULD NOT be enabled" + + Output := tests with input as { + "conn_filter": [ + { + "IPAllowList" : [], + "EnableSafeList": false, + "Name":"A" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableSafeList_Incorrect_V1 if { + ControlNumber := "EXO 2.12" + Requirement := "Safe lists SHOULD NOT be enabled" + + Output := tests with input as { + "conn_filter": [ + { + "IPAllowList" : [], + "EnableSafeList": true, + "Name" : "A" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableSafeList_Correct_V2 if { + ControlNumber := "EXO 2.12" + Requirement := "Safe lists SHOULD NOT be enabled" + + Output := tests with input as { + "conn_filter": [ + { + "IPAllowList" : ["this.shouldnt.matter"], + "EnableSafeList": false, + "Name":"A" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/EXO/EXOConfig2_13_test.rego b/Testing/Unit/Rego/EXO/EXOConfig2_13_test.rego new file mode 100644 index 0000000000..b83f060a4d --- /dev/null +++ b/Testing/Unit/Rego/EXO/EXOConfig2_13_test.rego @@ -0,0 +1,49 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_AuditDisabled_Correct if { + ControlNumber := "EXO 2.13" + Requirement := "Mailbox auditing SHALL be enabled" + + Output := tests with input as { + "org_config": + [ + { + "AuditDisabled" : false, + "Identity" : "Test name", + "Name":"A" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AuditDisabled_Incorrect if { + ControlNumber := "EXO 2.13" + Requirement := "Mailbox auditing SHALL be enabled" + + Output := tests with input as { + "org_config": [ + { + "AuditDisabled" : true, + "Identity" : "Test name", + "Name" : "A" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/EXO/EXOConfig2_14_test.rego b/Testing/Unit/Rego/EXO/EXOConfig2_14_test.rego new file mode 100644 index 0000000000..8cba3792bf --- /dev/null +++ b/Testing/Unit/Rego/EXO/EXOConfig2_14_test.rego @@ -0,0 +1,51 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_3rdParty_Correct_V1 if { + ControlNumber := "EXO 2.14" + Requirement := "A spam filter SHALL be enabled. The filtering solution selected SHOULD offer services comparable to the native spam filtering offered by Microsoft" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} + +# +# Policy 2 +#-- +test_3rdParty_Correct_V2 if { + ControlNumber := "EXO 2.14" + Requirement := "Spam and high confidence spam SHALL be moved to either the junk email folder or the quarantine folder" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} + +# +# Policy 3 +#-- +test_3rdParty_Correct_V3 if { + ControlNumber := "EXO 2.14" + Requirement := "Allowed senders MAY be added, but allowed domains SHALL NOT be added" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/EXO/EXOConfig2_15_test.rego b/Testing/Unit/Rego/EXO/EXOConfig2_15_test.rego new file mode 100644 index 0000000000..24ef2cbab6 --- /dev/null +++ b/Testing/Unit/Rego/EXO/EXOConfig2_15_test.rego @@ -0,0 +1,51 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_3rdParty_Correct_V1 if { + ControlNumber := "EXO 2.15" + Requirement := "URL comparison with a block-list SHOULD be enabled" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} + +# +# Policy 2 +#-- +test_3rdParty_Correct_V2 if { + ControlNumber := "EXO 2.15" + Requirement := "Direct download links SHOULD be scanned for malware" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} + +# +# Policy 3 +#-- +test_3rdParty_Correct_V3 if { + ControlNumber := "EXO 2.15" + Requirement := "User click tracking SHOULD be enabled" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/EXO/EXOConfig2_16_test.rego b/Testing/Unit/Rego/EXO/EXOConfig2_16_test.rego new file mode 100644 index 0000000000..182df67c11 --- /dev/null +++ b/Testing/Unit/Rego/EXO/EXOConfig2_16_test.rego @@ -0,0 +1,35 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_3rdParty_Correct_V1 if { + ControlNumber := "EXO 2.16" + Requirement := "At a minimum, the following alerts SHALL be enabled...[see Exchange Online secure baseline for list]" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} + +# +# Policy 2 +#-- +test_3rdParty_Correct_V2 if { + ControlNumber := "EXO 2.16" + Requirement := "The alerts SHOULD be sent to a monitored address or incorporated into a SIEM" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/EXO/EXOConfig2_17_test.rego b/Testing/Unit/Rego/EXO/EXOConfig2_17_test.rego new file mode 100644 index 0000000000..3a3c88b227 --- /dev/null +++ b/Testing/Unit/Rego/EXO/EXOConfig2_17_test.rego @@ -0,0 +1,51 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_3rdParty_Correct_V1 if { + ControlNumber := "EXO 2.17" + Requirement := "Unified audit logging SHALL be enabled" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} + +# +# Policy 2 +#-- +test_3rdParty_Correct_V2 if { + ControlNumber := "EXO 2.17" + Requirement := "Advanced audit SHALL be enabled" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} + +# +# Policy 3 +#-- +test_3rdParty_Correct_V3 if { + ControlNumber := "EXO 2.17" + Requirement := "Audit logs SHALL be maintained for at least the minimum duration dictated by OMB M-21-31" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/OneDrive/OneDriveConfig2_01_test.rego b/Testing/Unit/Rego/OneDrive/OneDriveConfig2_01_test.rego new file mode 100644 index 0000000000..39c2e7912d --- /dev/null +++ b/Testing/Unit/Rego/OneDrive/OneDriveConfig2_01_test.rego @@ -0,0 +1,44 @@ +package onedrive +import future.keywords + + +# +# Policy 1 +#-- +test_OneDriveLoopSharingCapability_Correct if { + ControlNumber := "OneDrive 2.1" + Requirement := "Anyone links SHOULD be disabled" + + Output := tests with input as { + "SPO_tenant_info": [ + { + "OneDriveLoopSharingCapability" : 1 + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_OneDriveLoopSharingCapability_Incorrect if { + ControlNumber := "OneDrive 2.1" + Requirement := "Anyone links SHOULD be disabled" + + Output := tests with input as { + "SPO_tenant_info": [ + { + "OneDriveLoopSharingCapability" : 2 + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} diff --git a/Testing/Unit/Rego/OneDrive/OneDriveConfig2_02_test.rego b/Testing/Unit/Rego/OneDrive/OneDriveConfig2_02_test.rego new file mode 100644 index 0000000000..4d212a0f79 --- /dev/null +++ b/Testing/Unit/Rego/OneDrive/OneDriveConfig2_02_test.rego @@ -0,0 +1,105 @@ +package onedrive +import future.keywords + + +# +# Policy 1 +#-- +test_ExternalUserExpirationRequired_Correct if { + ControlNumber := "OneDrive 2.2" + Requirement := "An expiration date SHOULD be set for Anyone links" + + Output := tests with input as { + "SPO_tenant_info": [ + { + "ExternalUserExpirationRequired" : true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ExternalUserExpirationRequired_Incorrect if { + ControlNumber := "OneDrive 2.2" + Requirement := "An expiration date SHOULD be set for Anyone links" + + Output := tests with input as { + "SPO_tenant_info": [ + { + "ExternalUserExpirationRequired" : false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + + +# +# Policy 2 +#-- +test_ExternalUserExpireInDays_Correct if { + ControlNumber := "OneDrive 2.2" + Requirement := "Expiration date SHOULD be set to thirty days" + + Output := tests with input as { + "SPO_tenant_info": [ + { + "ExternalUserExpireInDays" : 30 + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ExternalUserExpireInDays_Incorrect_V1 if { + ControlNumber := "OneDrive 2.2" + Requirement := "Expiration date SHOULD be set to thirty days" + + Output := tests with input as { + "SPO_tenant_info": [ + { + "ExternalUserExpireInDays" : 31 + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_ExternalUserExpireInDays_Incorrect_V2 if { + ControlNumber := "OneDrive 2.2" + Requirement := "Expiration date SHOULD be set to thirty days" + + Output := tests with input as { + "SPO_tenant_info": [ + { + "ExternalUserExpireInDays" : 29 + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} diff --git a/Testing/Unit/Rego/OneDrive/OneDriveConfig2_03_test.rego b/Testing/Unit/Rego/OneDrive/OneDriveConfig2_03_test.rego new file mode 100644 index 0000000000..3d21936484 --- /dev/null +++ b/Testing/Unit/Rego/OneDrive/OneDriveConfig2_03_test.rego @@ -0,0 +1,44 @@ +package onedrive +import future.keywords + + +# +# Policy 1 +#-- +test_DefaultLinkPermission_Correct if { + ControlNumber := "OneDrive 2.3" + Requirement := "Anyone link permissions SHOULD be limited to View" + + Output := tests with input as { + "SPO_tenant_info": [ + { + "DefaultLinkPermission" : 1 + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_DefaultLinkPermission_Incorrect if { + ControlNumber := "OneDrive 2.3" + Requirement := "Anyone link permissions SHOULD be limited to View" + + Output := tests with input as { + "SPO_tenant_info": [ + { + "DefaultLinkPermission" : 2 + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} diff --git a/Testing/Unit/Rego/OneDrive/OneDriveConfig2_04_test.rego b/Testing/Unit/Rego/OneDrive/OneDriveConfig2_04_test.rego new file mode 100644 index 0000000000..0a91d91bc1 --- /dev/null +++ b/Testing/Unit/Rego/OneDrive/OneDriveConfig2_04_test.rego @@ -0,0 +1,69 @@ +package onedrive +import future.keywords + + +# +# Policy 1 +#-- +test_AllowedDomainList_Correct_V1 if { + ControlNumber := "OneDrive 2.4" + Requirement := "OneDrive Client for Windows SHALL be restricted to agency-Defined Domain(s)" + + Output := tests with input as { + "Tenant_sync_info": [ + { + "AllowedDomainList": [ + "786548dd-877b-4760-a749-6b1efbc1190a" + ] + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowedDomainList_Correct_V2 if { + ControlNumber := "OneDrive 2.4" + Requirement := "OneDrive Client for Windows SHALL be restricted to agency-Defined Domain(s)" + + Output := tests with input as { + "Tenant_sync_info": [ + { + "AllowedDomainList": [ + "786548dd-877b-4760-a749-6b1efbc1190a", + "877564ff-877b-4760-a749-6b1efbc1190a", + "c64580cf-5b99-4c0a-b15b-db035c63e177" + ] + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowedDomainList_Incorrect if { + ControlNumber := "OneDrive 2.4" + Requirement := "OneDrive Client for Windows SHALL be restricted to agency-Defined Domain(s)" + + Output := tests with input as { + "Tenant_sync_info": [ + { + "AllowedDomainList": [ ] + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} diff --git a/Testing/Unit/Rego/OneDrive/OneDriveConfig2_05_test.rego b/Testing/Unit/Rego/OneDrive/OneDriveConfig2_05_test.rego new file mode 100644 index 0000000000..99c90ff230 --- /dev/null +++ b/Testing/Unit/Rego/OneDrive/OneDriveConfig2_05_test.rego @@ -0,0 +1,44 @@ +package onedrive +import future.keywords + + +# +# Policy 1 +#-- +test_BlockMacSync_Correct if { + ControlNumber := "OneDrive 2.5" + Requirement := "OneDrive Client Sync SHALL only be allowed only within the local domain" + + Output := tests with input as { + "Tenant_sync_info": [ + { + "BlockMacSync" : false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_BlockMacSync_Incorrect if { + ControlNumber := "OneDrive 2.5" + Requirement := "OneDrive Client Sync SHALL only be allowed only within the local domain" + + Output := tests with input as { + "Tenant_sync_info": [ + { + "BlockMacSync" : true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} diff --git a/Testing/Unit/Rego/OneDrive/OneDriveConfig2_06_test.rego b/Testing/Unit/Rego/OneDrive/OneDriveConfig2_06_test.rego new file mode 100644 index 0000000000..aa0a29a35a --- /dev/null +++ b/Testing/Unit/Rego/OneDrive/OneDriveConfig2_06_test.rego @@ -0,0 +1,19 @@ +package onedrive +import future.keywords + + +# +# Policy 1 +#-- +test_NotImplemented_Correct if { + ControlNumber := "OneDrive 2.6" + Requirement := "OneDrive Client Sync SHALL be restricted to the local domain" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Onedrive Secure Configuration Baseline policy 2.6 for instructions on manual check" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/OneDrive/OneDriveConfig2_07_test.rego b/Testing/Unit/Rego/OneDrive/OneDriveConfig2_07_test.rego new file mode 100644 index 0000000000..bd9370cd2a --- /dev/null +++ b/Testing/Unit/Rego/OneDrive/OneDriveConfig2_07_test.rego @@ -0,0 +1,19 @@ +package onedrive +import future.keywords + + +# +# Policy 1 +#-- +test_NotImplemented_Correct if { + ControlNumber := "OneDrive 2.7" + Requirement := "Legacy Authentication SHALL be blocked" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Onedrive Secure Configuration Baseline policy 2.7 for instructions on manual check" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/PowerPlatform/PowerPlatformConfig2_01_test.rego b/Testing/Unit/Rego/PowerPlatform/PowerPlatformConfig2_01_test.rego new file mode 100644 index 0000000000..cfb0e3a70f --- /dev/null +++ b/Testing/Unit/Rego/PowerPlatform/PowerPlatformConfig2_01_test.rego @@ -0,0 +1,40 @@ +package powerplatform +import future.keywords + + +# +# Policy 1 +#-- +test_disableEnvironmentCreationByNonAdminUsers_Correct if { + ControlNumber := "Power Platform 2.1" + Requirement := "The ability to create additional environments SHALL be restricted to admins" + + Output := tests with input as { + "environment_creation": { + "disableEnvironmentCreationByNonAdminUsers" : true + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_disableEnvironmentCreationByNonAdminUsers_Incorrect if { + ControlNumber := "Power Platform 2.1" + Requirement := "The ability to create additional environments SHALL be restricted to admins" + + Output := tests with input as { + "environment_creation": { + "disableEnvironmentCreationByNonAdminUsers" : false + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} diff --git a/Testing/Unit/Rego/PowerPlatform/PowerPlatformConfig2_02_test.rego b/Testing/Unit/Rego/PowerPlatform/PowerPlatformConfig2_02_test.rego new file mode 100644 index 0000000000..a7818e978f --- /dev/null +++ b/Testing/Unit/Rego/PowerPlatform/PowerPlatformConfig2_02_test.rego @@ -0,0 +1,293 @@ +package powerplatform +import future.keywords + + +# +# Policy 1 +#-- +test_name_Correct if { + ControlNumber := "Power Platform 2.2" + Requirement := "A DLP policy SHALL be created to restrict connector access in the default Power Platform environment" + + Output := tests with input as { + "tenant_id": "Test Id", + "dlp_policies": { + "value": [{ + "displayName": "Block Third-Party Connectors", + "environments": [{ + "name": "Default-Test Id" + }] + }] + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_name_Incorrect if { + ControlNumber := "Power Platform 2.2" + Requirement := "A DLP policy SHALL be created to restrict connector access in the default Power Platform environment" + + Output := tests with input as { + "tenant_id": "Test Id", + "dlp_policies": { + "value": [{ + "displayName": "Block Third-Party Connectors", + "environments": [{ + "name": "NotDefault-Test Id" + }] + }] + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No policy found that applies to default environment" +} + +# +# Policy 2 +#-- +test_environment_list_Correct if { + ControlNumber := "Power Platform 2.2" + Requirement := "Non-default environments SHOULD have at least one DLP policy that affects them" + + Output := tests with input as { + "dlp_policies": { + "value": [{ + "displayName": "Block Third-Party Connectors", + "environments": [{ + "name": "Default-Test Id" + }] + }] + }, + "environment_list": [{ + "EnvironmentName": "Default-Test Id" + }] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_environment_list_Incorrect if { + ControlNumber := "Power Platform 2.2" + Requirement := "Non-default environments SHOULD have at least one DLP policy that affects them" + + Output := tests with input as { + "dlp_policies": { + "value": [{ + "displayName": "Block Third-Party Connectors", + "environments": [{ + "name": "Default-Test Id" + }] + }] + }, + "environment_list": [{ + "EnvironmentName": "Default-Test Id" + + }, + { + "EnvironmentName": "Test1" + + }] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 Subsequent environments without DLP policies: Test1" +} + +# +# Policy 3 +#-- +test_classification_Correct_V1 if { + ControlNumber := "Power Platform 2.2" + Requirement := "All connectors except those listed...[see Power Platform secure baseline for list]...SHOULD be added to the Blocked category in the default environment policy" + + Output := tests with input as { + "tenant_id": "Test Id", + "dlp_policies": { + "value": [{ + "connectorGroups": [{ + "classification": "Confidential", + "connectors": [{ + "id": "/providers/Microsoft.PowerApps/apis/shared_powervirtualagents" + }] + }], + "environments": [{ + "name": "Default-Test Id" + }] + }] + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_classification_Correct_V2 if { + ControlNumber := "Power Platform 2.2" + Requirement := "All connectors except those listed...[see Power Platform secure baseline for list]...SHOULD be added to the Blocked category in the default environment policy" + + Output := tests with input as { + "tenant_id": "Test Id", + "dlp_policies": { + "value": [{ + "connectorGroups": [{ + "classification": "General", + "connectors": [{ + "id": "/providers/Microsoft.PowerApps/apis/shared_powervirtualagents" + }] + }], + "environments": [{ + "name": "Default-Test Id" + }] + }] + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_connectorGroups_Correct if { + ControlNumber := "Power Platform 2.2" + Requirement := "All connectors except those listed...[see Power Platform secure baseline for list]...SHOULD be added to the Blocked category in the default environment policy" + + Output := tests with input as { + "tenant_id": "Test Id", + "dlp_policies": { + "value": [{ + "connectorGroups": [{ + "classification": "Confidential", + "connectors": [{ + "id": "/providers/Microsoft.PowerApps/apis/shared_powervirtualagents" + }] + }, + { + "classification": "General", + "connectors": [{ + "id": "/providers/Microsoft.PowerApps/apis/shared_powervirtualagents" + }] + }], + "environments": [{ + "name": "Default-Test Id" + }] + }] + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_classification_Incorrect_V1 if { + ControlNumber := "Power Platform 2.2" + Requirement := "All connectors except those listed...[see Power Platform secure baseline for list]...SHOULD be added to the Blocked category in the default environment policy" + + Output := tests with input as { + "tenant_id": "Test Id", + "dlp_policies": { + "value": [{ + "connectorGroups": [{ + "classification": "Confidential", + "connectors": [{ + "id": "HttpWebhook" + }] + }], + "environments": [{ + "name": "Default-Test Id" + }] + }] + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 Connectors are allowed that should be blocked: HttpWebhook" +} + +test_classification_Incorrect_V2 if { + ControlNumber := "Power Platform 2.2" + Requirement := "All connectors except those listed...[see Power Platform secure baseline for list]...SHOULD be added to the Blocked category in the default environment policy" + + Output := tests with input as { + "tenant_id": "Test Id", + "dlp_policies": { + "value": [{ + "connectorGroups": [{ + "classification": "General", + "connectors": [{ + "id": "HttpWebhook" + }] + }], + "environments": [{ + "name": "Default-Test Id" + }] + }] + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 Connectors are allowed that should be blocked: HttpWebhook" +} + +test_connectorGroups_Incorrect if { + ControlNumber := "Power Platform 2.2" + Requirement := "All connectors except those listed...[see Power Platform secure baseline for list]...SHOULD be added to the Blocked category in the default environment policy" + + Output := tests with input as { + "tenant_id": "Test Id", + "dlp_policies": { + "value": [{ + "connectorGroups": [{ + "classification": "Confidential", + "connectors": [{ + "id": "HttpWebhook" + }] + }, + { + "classification": "General", + "connectors": [{ + "id": "HttpWebhook" + }] + }], + "environments": [{ + "name": "Default-Test Id" + }] + }] + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 Connectors are allowed that should be blocked: HttpWebhook" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/PowerPlatform/PowerPlatformConfig2_03_test.rego b/Testing/Unit/Rego/PowerPlatform/PowerPlatformConfig2_03_test.rego new file mode 100644 index 0000000000..d57481b9d8 --- /dev/null +++ b/Testing/Unit/Rego/PowerPlatform/PowerPlatformConfig2_03_test.rego @@ -0,0 +1,60 @@ +package powerplatform +import future.keywords + + +# +# Policy 1 +#-- +test_isDisabled_Correct if { + ControlNumber := "Power Platform 2.3" + Requirement := "Power Platform tenant isolation SHALL be enabled" + + Output := tests with input as { + "tenant_isolation": { + "properties" : { + "isDisabled" : false + } + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_isDisabled_Incorrect if { + ControlNumber := "Power Platform 2.3" + Requirement := "Power Platform tenant isolation SHALL be enabled" + + Output := tests with input as { + "tenant_isolation": { + "properties" : { + "isDisabled" : true + } + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +# +# Policy 2 +#-- +test_NotImplemented_Correct if { + ControlNumber := "Power Platform 2.3" + Requirement := "An inbound/outbound connection allowlist SHOULD be configured" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Power Platform Secure Configuration Baseline policy 2.3 for instructions on manual check" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/PowerPlatform/PowerPlatformConfig2_04_test.rego b/Testing/Unit/Rego/PowerPlatform/PowerPlatformConfig2_04_test.rego new file mode 100644 index 0000000000..fd1076375c --- /dev/null +++ b/Testing/Unit/Rego/PowerPlatform/PowerPlatformConfig2_04_test.rego @@ -0,0 +1,19 @@ +package powerplatform +import future.keywords + + +# +# Policy 1 +#-- +test_NotImplemented_Correct if { + ControlNumber := "Power Platform 2.4" + Requirement := "Content security policies for model-driven Power Apps SHALL be enabled" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Power Platform Secure Configuration Baseline policy 2.4 for instructions on manual check" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/Sharepoint/SharepointConfig2_01_test.rego b/Testing/Unit/Rego/Sharepoint/SharepointConfig2_01_test.rego new file mode 100644 index 0000000000..6dbf62e6b1 --- /dev/null +++ b/Testing/Unit/Rego/Sharepoint/SharepointConfig2_01_test.rego @@ -0,0 +1,44 @@ +package sharepoint +import future.keywords + + +# +# Policy 1 +#-- +test_DefaultSharingLinkType_Correct if { + ControlNumber := "Sharepoint 2.1" + Requirement := "File and folder links default sharing setting SHALL be set to \"Specific People (Only the People the User Specifies)\"" + + Output := tests with input as { + "SPO_tenant": [ + { + "DefaultSharingLinkType" : 1 + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_DefaultSharingLinkType_Incorrect if { + ControlNumber := "Sharepoint 2.1" + Requirement := "File and folder links default sharing setting SHALL be set to \"Specific People (Only the People the User Specifies)\"" + + Output := tests with input as { + "SPO_tenant": [ + { + "DefaultSharingLinkType" : 2 + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} diff --git a/Testing/Unit/Rego/Sharepoint/SharepointConfig2_02_test.rego b/Testing/Unit/Rego/Sharepoint/SharepointConfig2_02_test.rego new file mode 100644 index 0000000000..e6e4f451e9 --- /dev/null +++ b/Testing/Unit/Rego/Sharepoint/SharepointConfig2_02_test.rego @@ -0,0 +1,44 @@ +package sharepoint +import future.keywords + + +# +# Policy 1 +#-- +test_SharingCapability_Correct if { + ControlNumber := "Sharepoint 2.2" + Requirement := "External sharing SHOULD be limited to approved domains and security groups per interagency collaboration needs" + + Output := tests with input as { + "SPO_tenant": [ + { + "SharingCapability" : 1 + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_SharingCapability_Incorrect if { + ControlNumber := "Sharepoint 2.2" + Requirement := "External sharing SHOULD be limited to approved domains and security groups per interagency collaboration needs" + + Output := tests with input as { + "SPO_tenant": [ + { + "SharingCapability" : 2 + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} diff --git a/Testing/Unit/Rego/Sharepoint/SharepointConfig2_03_test.rego b/Testing/Unit/Rego/Sharepoint/SharepointConfig2_03_test.rego new file mode 100644 index 0000000000..a4a4fb52a6 --- /dev/null +++ b/Testing/Unit/Rego/Sharepoint/SharepointConfig2_03_test.rego @@ -0,0 +1,19 @@ +package sharepoint +import future.keywords + + +# +# Policy 1 +#-- +test_NotImplemented_Correct if { + ControlNumber := "Sharepoint 2.3" + Requirement := "Sharing settings for specific SharePoint sites SHOULD align to their sensitivity level" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Sharepoint Secure Configuration Baseline policy 2.3 for instructions on manual check" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/Sharepoint/SharepointConfig2_04_test.rego b/Testing/Unit/Rego/Sharepoint/SharepointConfig2_04_test.rego new file mode 100644 index 0000000000..3f7d3260c2 --- /dev/null +++ b/Testing/Unit/Rego/Sharepoint/SharepointConfig2_04_test.rego @@ -0,0 +1,104 @@ +package sharepoint +import future.keywords + + +# +# Policy 1 +#-- +test_ExternalUserExpirationRequired_Correct if { + ControlNumber := "Sharepoint 2.4" + Requirement := "Expiration timers for 'guest access to a site or OneDrive' and 'people who use a verification code' SHOULD be set" + + Output := tests with input as { + "SPO_tenant": [ + { + "ExternalUserExpirationRequired" : true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ExternalUserExpirationRequired_Incorrect if { + ControlNumber := "Sharepoint 2.4" + Requirement := "Expiration timers for 'guest access to a site or OneDrive' and 'people who use a verification code' SHOULD be set" + + Output := tests with input as { + "SPO_tenant": [ + { + "ExternalUserExpirationRequired" : false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +# +# Policy 2 +#-- +test_ExternalUserExpireInDays_Correct if { + ControlNumber := "Sharepoint 2.4" + Requirement := "Expiration timers SHOULD be set to 30 days" + + Output := tests with input as { + "SPO_tenant": [ + { + "ExternalUserExpireInDays" : 30 + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ExternalUserExpireInDays_Incorrect_V1 if { + ControlNumber := "Sharepoint 2.4" + Requirement := "Expiration timers SHOULD be set to 30 days" + + Output := tests with input as { + "SPO_tenant": [ + { + "ExternalUserExpireInDays" : 29 + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_ExternalUserExpireInDays_Incorrect_V2 if { + ControlNumber := "Sharepoint 2.4" + Requirement := "Expiration timers SHOULD be set to 30 days" + + Output := tests with input as { + "SPO_tenant": [ + { + "ExternalUserExpireInDays" : 31 + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} diff --git a/Testing/Unit/Rego/Sharepoint/SharepointConfig2_05_test.rego b/Testing/Unit/Rego/Sharepoint/SharepointConfig2_05_test.rego new file mode 100644 index 0000000000..54166eaeac --- /dev/null +++ b/Testing/Unit/Rego/Sharepoint/SharepointConfig2_05_test.rego @@ -0,0 +1,59 @@ +package sharepoint +import future.keywords + +# +# Policy 1 +#-- +test_NotImplemented_Correct if { + ControlNumber := "Sharepoint 2.5" + Requirement := "Users SHALL be prevented from running custom scripts on personal sites (OneDrive)" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Sharepoint Secure Configuration Baseline policy 2.5 for instructions on manual check" +} + +# +# Policy 2 +#-- +test_DenyAddAndCustomizePages_Correct if { + ControlNumber := "Sharepoint 2.5" + Requirement := "Users SHALL be prevented from running custom scripts on self-service created sites" + + Output := tests with input as { + "SPO_site": [ + { + "DenyAddAndCustomizePages" : 2 + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_DenyAddAndCustomizePages_Incorrect if { + ControlNumber := "Sharepoint 2.5" + Requirement := "Users SHALL be prevented from running custom scripts on self-service created sites" + + Output := tests with input as { + "SPO_site": [ + { + "DenyAddAndCustomizePages" : 1 + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} diff --git a/Testing/Unit/Rego/Teams/TeamsConfig2_01_test.rego b/Testing/Unit/Rego/Teams/TeamsConfig2_01_test.rego new file mode 100644 index 0000000000..605e7ec827 --- /dev/null +++ b/Testing/Unit/Rego/Teams/TeamsConfig2_01_test.rego @@ -0,0 +1,112 @@ +package teams +import future.keywords + + +# +# Policy 1 +#-- +test_ExternalParticipantControl_Correct_V1 if { + ControlNumber := "Teams 2.1" + Requirement := "External participants SHOULD NOT be enabled to request control of shared desktops or windows in the Global (Org-wide default) meeting policy or in custom meeting policies if any exist" + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowExternalParticipantGiveRequestControl" : false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ExternalParticipantControl_Correct_V2 if { + ControlNumber := "Teams 2.1" + Requirement := "External participants SHOULD NOT be enabled to request control of shared desktops or windows in the Global (Org-wide default) meeting policy or in custom meeting policies if any exist" + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Tag:FirstCustomPolicy", + "AllowExternalParticipantGiveRequestControl" : false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ExternalParticipantControl_Incorrect_V1 if { + ControlNumber := "Teams 2.1" + Requirement := "External participants SHOULD NOT be enabled to request control of shared desktops or windows in the Global (Org-wide default) meeting policy or in custom meeting policies if any exist" + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowExternalParticipantGiveRequestControl" : true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) found that allows external control: Global" +} + +test_ExternalParticipantControl_Incorrect_V2 if { + ControlNumber := "Teams 2.1" + Requirement := "External participants SHOULD NOT be enabled to request control of shared desktops or windows in the Global (Org-wide default) meeting policy or in custom meeting policies if any exist" + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Tag:FirstCustomPolicy", + "AllowExternalParticipantGiveRequestControl" : true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) found that allows external control: Tag:FirstCustomPolicy" +} + +test_ExternalParticipantControl_MultiplePolicies if { + ControlNumber := "Teams 2.1" + Requirement := "External participants SHOULD NOT be enabled to request control of shared desktops or windows in the Global (Org-wide default) meeting policy or in custom meeting policies if any exist" + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowExternalParticipantGiveRequestControl" : true + }, + { + "Identity": "Tag:FirstCustomPolicy", + "AllowExternalParticipantGiveRequestControl" : false + }, + { + "Identity": "Tag:SecondCustomPolicy", + "AllowExternalParticipantGiveRequestControl" : true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + startswith(RuleOutput[0].ReportDetails, "2 meeting policy(ies) found that allows external control: ") + contains(RuleOutput[0].ReportDetails, "Global") # Not sure if we can assume the order these will appear in, + # hence the "contains" instead of a simple "==" + contains(RuleOutput[0].ReportDetails, "Tag:SecondCustomPolicy") +} diff --git a/Testing/Unit/Rego/Teams/TeamsConfig2_02_test.rego b/Testing/Unit/Rego/Teams/TeamsConfig2_02_test.rego new file mode 100644 index 0000000000..39676b227f --- /dev/null +++ b/Testing/Unit/Rego/Teams/TeamsConfig2_02_test.rego @@ -0,0 +1,117 @@ +package teams +import future.keywords + + +# +# Policy 1 +#-- +test_AnonymousMeetingStart_Correct_V1 if { + ControlNumber := "Teams 2.2" + Requirement := "Anonymous users SHALL NOT be enabled to start meetings in the Global (Org-wide default) meeting policy or in custom meeting policies if any exist" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowAnonymousUsersToStartMeeting" : false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AnonymousMeetingStart_Correct_V2 if { + ControlNumber := "Teams 2.2" + Requirement := "Anonymous users SHALL NOT be enabled to start meetings in the Global (Org-wide default) meeting policy or in custom meeting policies if any exist" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Tag:FirstCustomPolicy", + "AllowAnonymousUsersToStartMeeting" : false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AnonymousMeetingStart_Incorrect_V1 if { + ControlNumber := "Teams 2.2" + Requirement := "Anonymous users SHALL NOT be enabled to start meetings in the Global (Org-wide default) meeting policy or in custom meeting policies if any exist" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowAnonymousUsersToStartMeeting" : true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) found that allows anonymous users to start meetings: Global" +} + +test_AnonymousMeetingStart_Incorrect_V2 if { + ControlNumber := "Teams 2.2" + Requirement := "Anonymous users SHALL NOT be enabled to start meetings in the Global (Org-wide default) meeting policy or in custom meeting policies if any exist" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Tag:FirstCustomPolicy", + "AllowAnonymousUsersToStartMeeting" : true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) found that allows anonymous users to start meetings: Tag:FirstCustomPolicy" +} + +test_AnonymousMeetingStart_MultiplePolicies if { + ControlNumber := "Teams 2.2" + Requirement := "Anonymous users SHALL NOT be enabled to start meetings in the Global (Org-wide default) meeting policy or in custom meeting policies if any exist" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowAnonymousUsersToStartMeeting" : true + }, + { + "Identity": "Tag:FirstCustomPolicy", + "AllowAnonymousUsersToStartMeeting" : false + }, + { + "Identity": "Tag:SecondCustomPolicy", + "AllowAnonymousUsersToStartMeeting" : true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + startswith(RuleOutput[0].ReportDetails, "2 meeting policy(ies) found that allows anonymous users to start meetings: ") + contains(RuleOutput[0].ReportDetails, "Global") # Not sure if we can assume the order these will appear in, + # hence the "contains" instead of a simple "==" + contains(RuleOutput[0].ReportDetails, "Tag:SecondCustomPolicy") +} \ No newline at end of file diff --git a/Testing/Unit/Rego/Teams/TeamsConfig2_03_test.rego b/Testing/Unit/Rego/Teams/TeamsConfig2_03_test.rego new file mode 100644 index 0000000000..d4c416043f --- /dev/null +++ b/Testing/Unit/Rego/Teams/TeamsConfig2_03_test.rego @@ -0,0 +1,179 @@ +package teams +import future.keywords + + +# +# Policy 1 +#-- +test_meeting_policies_Correct if { + ControlNumber := "Teams 2.3" + Requirement := "Anonymous users, including dial-in users, SHOULD NOT be admitted automatically" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowPSTNUsersToBypassLobby": false, + "AutoAdmittedUsers": "EveryoneInCompany" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowPSTNUsersToBypassLobby_Incorrect if { + ControlNumber := "Teams 2.3" + Requirement := "Anonymous users, including dial-in users, SHOULD NOT be admitted automatically" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowPSTNUsersToBypassLobby": true, + "AutoAdmittedUsers": "EveryoneInCompany" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met: Dial-in users are enabled to bypass the lobby" +} + +test_AutoAdmittedUsers_Incorrect if { + ControlNumber := "Teams 2.3" + Requirement := "Anonymous users, including dial-in users, SHOULD NOT be admitted automatically" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowPSTNUsersToBypassLobby": true, + "AutoAdmittedUsers": "Everyone" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met: All users are admitted automatically" +} + +# It shouldn't matter that the custom policy is incorrect as this policy only applies to the Global policy +test_Multiple_Correct if { + ControlNumber := "Teams 2.3" + Requirement := "Anonymous users, including dial-in users, SHOULD NOT be admitted automatically" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowPSTNUsersToBypassLobby": false, + "AutoAdmittedUsers": "EveryoneInCompany" + }, + { + "Identity": "Tag:CustomPolicy", + "AllowPSTNUsersToBypassLobby": true, + "AutoAdmittedUsers": "Everyone" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +# +# Policy 2 +#-- +test_AutoAdmittedUsers_Correct_V1 if { + ControlNumber := "Teams 2.3" + Requirement := "Internal users SHOULD be admitted automatically" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AutoAdmittedUsers" : "EveryoneInSameAndFederatedCompany" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AutoAdmittedUsers_Correct_V2 if { + ControlNumber := "Teams 2.3" + Requirement := "Internal users SHOULD be admitted automatically" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AutoAdmittedUsers" : "EveryoneInCompanyExcludingGuests" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AutoAdmittedUsers_Incorrect_V2 if { + ControlNumber := "Teams 2.3" + Requirement := "Internal users SHOULD be admitted automatically" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AutoAdmittedUsers" : "OrganizerOnly" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_AutoAdmittedUsers_Incorrect_V3 if { + ControlNumber := "Teams 2.3" + Requirement := "Internal users SHOULD be admitted automatically" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AutoAdmittedUsers" : "InvitedUsers" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/Teams/TeamsConfig2_04_test.rego b/Testing/Unit/Rego/Teams/TeamsConfig2_04_test.rego new file mode 100644 index 0000000000..5c3b20dfe1 --- /dev/null +++ b/Testing/Unit/Rego/Teams/TeamsConfig2_04_test.rego @@ -0,0 +1,332 @@ +package teams +import future.keywords + + +# +# Policy 1 +#-- +test_AllowFederatedUsers_Correct_V1 if { + ControlNumber := "Teams 2.4" + Requirement := "External access SHALL only be enabled on a per-domain basis" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowFederatedUsers" : false, + "AllowedDomains": [] + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowFederatedUsers_Correct_V2 if { + ControlNumber := "Teams 2.4" + Requirement := "External access SHALL only be enabled on a per-domain basis" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowFederatedUsers" : false, + "AllowedDomains": [ + { + "AllowedDomain": ["Domain=test365.cisa.dhs.gov"] + } + ] + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowedDomains_Correct if { + ControlNumber := "Teams 2.4" + Requirement := "External access SHALL only be enabled on a per-domain basis" + + Output := tests with input as { + "federation_configuration":[ + { + "Identity": "Global", + "AllowFederatedUsers" : true, + "AllowedDomains": [ + { + "AllowedDomain": ["Domain=test365.cisa.dhs.gov"] + } + ] + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowedDomains_Incorrect if { + ControlNumber := "Teams 2.4" + Requirement := "External access SHALL only be enabled on a per-domain basis" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowFederatedUsers" : true, + "AllowedDomains": [] + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) that allow external access across all domains: Global" +} + +test_AllowFederatedUsers_Correct_V1_multi if { + ControlNumber := "Teams 2.4" + Requirement := "External access SHALL only be enabled on a per-domain basis" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowFederatedUsers" : false, + "AllowedDomains": [] + }, + { + "Identity": "Tag:AllOn", + "AllowFederatedUsers" : false, + "AllowedDomains": [] + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowFederatedUsers_Correct_V2_multi if { + ControlNumber := "Teams 2.4" + Requirement := "External access SHALL only be enabled on a per-domain basis" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowFederatedUsers" : false, + "AllowedDomains": [ + { + "AllowedDomain": ["Domain=test365.cisa.dhs.gov"] + } + ] + }, + { + "Identity": "Tag:AllOn", + "AllowFederatedUsers" : false, + "AllowedDomains": [ + { + "AllowedDomain": ["Domain=test365.cisa.dhs.gov"] + } + ] + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + + +test_AllowedDomains_Correct_multi if { + ControlNumber := "Teams 2.4" + Requirement := "External access SHALL only be enabled on a per-domain basis" + + Output := tests with input as { + "federation_configuration":[ + { + "Identity": "Global", + "AllowFederatedUsers" : true, + "AllowedDomains": [ + { + "AllowedDomain": ["Domain=test365.cisa.dhs.gov"] + } + ] + }, + { + "Identity": "Tag:AllOn", + "AllowFederatedUsers" : true, + "AllowedDomains": [ + { + "AllowedDomain": ["Domain=test365.cisa.dhs.gov"] + } + ] + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowedDomains_Incorrect_multi if { + ControlNumber := "Teams 2.4" + Requirement := "External access SHALL only be enabled on a per-domain basis" + + Output := tests with input as { + "federation_configuration":[ + { + "Identity": "Global", + "AllowFederatedUsers" : true, + "AllowedDomains": [] + }, + { + "Identity": "Tag:AllOn", + "AllowFederatedUsers" : true, + "AllowedDomains": [] + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "2 meeting policy(ies) that allow external access across all domains: Global, Tag:AllOn" +} + +# +# Policy 2 +#-- +test_AllowAnonymousUsersToJoinMeeting_Correct_V1 if { + ControlNumber := "Teams 2.4" + Requirement := "Anonymous users SHOULD be enabled to join meetings" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowAnonymousUsersToJoinMeeting" : true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowAnonymousUsersToJoinMeeting_Correct_V2 if { + ControlNumber := "Teams 2.4" + Requirement := "Anonymous users SHOULD be enabled to join meetings" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Tag:FirstCustomPolicy", + "AllowAnonymousUsersToJoinMeeting" : true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowAnonymousUsersToJoinMeeting_Incorrect_V1 if { + ControlNumber := "Teams 2.4" + Requirement := "Anonymous users SHOULD be enabled to join meetings" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowAnonymousUsersToJoinMeeting" : false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) found that don't allow anonymous users to join meetings: Global" +} + +test_AllowAnonymousUsersToJoinMeeting_Incorrect_V2 if { + ControlNumber := "Teams 2.4" + Requirement := "Anonymous users SHOULD be enabled to join meetings" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Tag:FirstCustomPolicy", + "AllowAnonymousUsersToJoinMeeting" : false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) found that don't allow anonymous users to join meetings: Tag:FirstCustomPolicy" +} + +test_AllowAnonymousUsersToJoinMeeting_MultiplePolicies if { + ControlNumber := "Teams 2.4" + Requirement := "Anonymous users SHOULD be enabled to join meetings" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowAnonymousUsersToJoinMeeting" : true + }, + { + "Identity": "Tag:FirstCustomPolicy", + "AllowAnonymousUsersToJoinMeeting" : false + }, + { + "Identity": "Tag:SecondCustomPolicy", + "AllowAnonymousUsersToJoinMeeting" : true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) found that don't allow anonymous users to join meetings: Tag:FirstCustomPolicy" +} + + diff --git a/Testing/Unit/Rego/Teams/TeamsConfig2_05_test.rego b/Testing/Unit/Rego/Teams/TeamsConfig2_05_test.rego new file mode 100644 index 0000000000..5f0d3c1d66 --- /dev/null +++ b/Testing/Unit/Rego/Teams/TeamsConfig2_05_test.rego @@ -0,0 +1,290 @@ +package teams +import future.keywords + + +# +# Policy 1 +#-- +test_AllowTeamsConsumerInbound_Correct_V1 if { + ControlNumber := "Teams 2.5" + Requirement := "Unmanaged users SHALL NOT be enabled to initiate contact with internal users" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowTeamsConsumer" : false, + "AllowTeamsConsumerInbound": false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowTeamsConsumerInbound_Correct_V1_multi if { + ControlNumber := "Teams 2.5" + Requirement := "Unmanaged users SHALL NOT be enabled to initiate contact with internal users" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowTeamsConsumer" : false, + "AllowTeamsConsumerInbound": false + }, + { + "Identity": "Tag:AllOn", + "AllowTeamsConsumer" : false, + "AllowTeamsConsumerInbound": false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowTeamsConsumerInbound_Correct_V2 if { + ControlNumber := "Teams 2.5" + Requirement := "Unmanaged users SHALL NOT be enabled to initiate contact with internal users" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowTeamsConsumer" : false, + "AllowTeamsConsumerInbound": true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowTeamsConsumerInbound_Correct_V2_multi if { + ControlNumber := "Teams 2.5" + Requirement := "Unmanaged users SHALL NOT be enabled to initiate contact with internal users" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowTeamsConsumer" : false, + "AllowTeamsConsumerInbound": true + }, + { + "Identity": "Tag:AllOn", + "AllowTeamsConsumer" : false, + "AllowTeamsConsumerInbound": true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowTeamsConsumer_Incorrect if { + ControlNumber := "Teams 2.5" + Requirement := "Unmanaged users SHALL NOT be enabled to initiate contact with internal users" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowTeamsConsumer" : true, + "AllowTeamsConsumerInbound": true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 Configuration allowed unmanaged users to initiate contact with internal user across domains: Global" +} + +test_AllowTeamsConsumer_Incorrect_multi if { + ControlNumber := "Teams 2.5" + Requirement := "Unmanaged users SHALL NOT be enabled to initiate contact with internal users" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowTeamsConsumer" : true, + "AllowTeamsConsumerInbound": true + }, + { + "Identity": "Tag:AllOn", + "AllowTeamsConsumer" : true, + "AllowTeamsConsumerInbound": true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "2 Configuration allowed unmanaged users to initiate contact with internal user across domains: Global, Tag:AllOn" +} + +test_AllowTeamsConsumer_Incorrect if { + ControlNumber := "Teams 2.5" + Requirement := "Unmanaged users SHALL NOT be enabled to initiate contact with internal users" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowTeamsConsumer" : true, + "AllowTeamsConsumerInbound": false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowTeamsConsumer_Incorrect_multi if { + ControlNumber := "Teams 2.5" + Requirement := "Unmanaged users SHALL NOT be enabled to initiate contact with internal users" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowTeamsConsumer" : true, + "AllowTeamsConsumerInbound": false + }, + { + "Identity": "Tag:AllOn", + "AllowTeamsConsumer" : true, + "AllowTeamsConsumerInbound": false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} +# +# Policy 2 +#-- +test_AllowTeamsConsumer_Correct if { + ControlNumber := "Teams 2.5" + Requirement := "Internal users SHOULD NOT be enabled to initiate contact with unmanaged users" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowTeamsConsumer" : false, + "AllowTeamsConsumerInbound": false # the value here doesn't matter for this control + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowTeamsConsumer_Correct_multi if { + ControlNumber := "Teams 2.5" + Requirement := "Internal users SHOULD NOT be enabled to initiate contact with unmanaged users" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowTeamsConsumer" : false, + "AllowTeamsConsumerInbound": false # the value here doesn't matter for this control + }, + { + "Identity": "Tag:AllOn", + "AllowTeamsConsumer" : false, + "AllowTeamsConsumerInbound": false # the value here doesn't matter for this control + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowTeamsConsumer_Incorrect if { + ControlNumber := "Teams 2.5" + Requirement := "Internal users SHOULD NOT be enabled to initiate contact with unmanaged users" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowTeamsConsumer" : true, + "AllowTeamsConsumerInbound": false # the value here doesn't matter for this control + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 Internal users are enabled to initiate contact with unmanaged users across domains: Global" +} + +test_AllowTeamsConsumer_Incorrect_multi if { + ControlNumber := "Teams 2.5" + Requirement := "Internal users SHOULD NOT be enabled to initiate contact with unmanaged users" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowTeamsConsumer" : true, + "AllowTeamsConsumerInbound": false # the value here doesn't matter for this control + }, + { + "Identity": "Tag:AllOn", + "AllowTeamsConsumer" : true, + "AllowTeamsConsumerInbound": false # the value here doesn't matter for this control + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "2 Internal users are enabled to initiate contact with unmanaged users across domains: Global, Tag:AllOn" +} diff --git a/Testing/Unit/Rego/Teams/TeamsConfig2_06_test.rego b/Testing/Unit/Rego/Teams/TeamsConfig2_06_test.rego new file mode 100644 index 0000000000..afc9e5147f --- /dev/null +++ b/Testing/Unit/Rego/Teams/TeamsConfig2_06_test.rego @@ -0,0 +1,94 @@ +package teams +import future.keywords + + +# +# Policy 1 +#-- +test_AllowPublicUsers_Correct if { + ControlNumber := "Teams 2.6" + Requirement := "Contact with Skype users SHALL be blocked" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowPublicUsers" : false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowPublicUsers_InCorrect if { + ControlNumber := "Teams 2.6" + Requirement := "Contact with Skype users SHALL be blocked" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowPublicUsers" : true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 domains that allows contact with Skype users: Global" +} + +test_AllowPublicUsers_Correct_multi if { + ControlNumber := "Teams 2.6" + Requirement := "Contact with Skype users SHALL be blocked" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowPublicUsers" : false + }, + { + "Identity": "Tag:AllOn", + "AllowPublicUsers" : false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowPublicUsers_InCorrect_multi if { + ControlNumber := "Teams 2.6" + Requirement := "Contact with Skype users SHALL be blocked" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowPublicUsers" : true + }, + { + "Identity": "Tag:AllOn", + "AllowPublicUsers" : true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "2 domains that allows contact with Skype users: Global, Tag:AllOn" +} diff --git a/Testing/Unit/Rego/Teams/TeamsConfig2_07_test.rego b/Testing/Unit/Rego/Teams/TeamsConfig2_07_test.rego new file mode 100644 index 0000000000..6d2951cf2e --- /dev/null +++ b/Testing/Unit/Rego/Teams/TeamsConfig2_07_test.rego @@ -0,0 +1,222 @@ +package teams +import future.keywords + + +# +# Policy 1 +#-- +test_AllowEmailIntoChannel_Correct_V1 if { + ControlNumber := "Teams 2.7" + Requirement := "Teams email integration SHALL be disabled" + + Output := tests with input as { + "client_configuration": [ + { + "Identity": "Global", + "AllowEmailIntoChannel": false + } + ], + "teams_tenant_info": [ + { + "ServiceInstance": "MicrosoftCommunicationsOnline/NOAM-ED6-A6" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowEmailIntoChannel_Correct_V1_multi if { + ControlNumber := "Teams 2.7" + Requirement := "Teams email integration SHALL be disabled" + + Output := tests with input as { + "client_configuration": [ + { + "Identity": "Global", + "AllowEmailIntoChannel": false + }, + { + "Identity": "Tag:AllOn", + "AllowEmailIntoChannel": false + } + ], + "teams_tenant_info": [ + { + "ServiceInstance": "MicrosoftCommunicationsOnline/NOAM-ED6-A6" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowEmailIntoChannel_Incorrect if { + ControlNumber := "Teams 2.7" + Requirement := "Teams email integration SHALL be disabled" + + Output := tests with input as { + "client_configuration": [ + { + "Identity": "Global", + "AllowEmailIntoChannel": true + } + ], + "teams_tenant_info": [ + { + "ServiceInstance": "MicrosoftCommunicationsOnline/NOAM-ED6-A6" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 Requirement not met: Email integration is enabled across domain: Global" +} + +test_AllowEmailIntoChannel_Incorrect_multi if { + ControlNumber := "Teams 2.7" + Requirement := "Teams email integration SHALL be disabled" + + Output := tests with input as { + "client_configuration": [ + { + "Identity": "Global", + "AllowEmailIntoChannel": true + }, + { + "Identity": "Tag:AllOn", + "AllowEmailIntoChannel": true + } + ], + "teams_tenant_info": [ + { + "ServiceInstance": "MicrosoftCommunicationsOnline/NOAM-ED6-A6" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "2 Requirement not met: Email integration is enabled across domain: Global, Tag:AllOn" +} + +test_AllowEmailIntoChannel_Correct_V2 if { + ControlNumber := "Teams 2.7" + Requirement := "Teams email integration SHALL be disabled" + + Output := tests with input as { + "client_configuration": [ + { + "Identity": "Global", + "AllowEmailIntoChannel": false + } + ], + "teams_tenant_info": [ + { + "ServiceInstance": "MicrosoftCommunicationsOnline/GOV-1B-G6" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "N/A: Feature is unavailable in GCC environments" +} + +test_AllowEmailIntoChannel_Correct_V2_multi if { + ControlNumber := "Teams 2.7" + Requirement := "Teams email integration SHALL be disabled" + + Output := tests with input as { + "client_configuration": [ + { + "Identity": "Global", + "AllowEmailIntoChannel": false + }, + { + "Identity": "Tag:AllOn", + "AllowEmailIntoChannel": false + } + ], + "teams_tenant_info": [ + { + "ServiceInstance": "MicrosoftCommunicationsOnline/GOV-1B-G6" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "N/A: Feature is unavailable in GCC environments" +} + +test_AllowEmailIntoChannel_Correct_V3 if { + ControlNumber := "Teams 2.7" + Requirement := "Teams email integration SHALL be disabled" + + Output := tests with input as { + "client_configuration": [ + { + "Identity": "Global", + "AllowEmailIntoChannel": true + } + ], + "teams_tenant_info": [ + { + "ServiceInstance": "MicrosoftCommunicationsOnline/GOV-1B-G6" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "N/A: Feature is unavailable in GCC environments" +} + +test_AllowEmailIntoChannel_Correct_V3 if { + ControlNumber := "Teams 2.7" + Requirement := "Teams email integration SHALL be disabled" + + Output := tests with input as { + "client_configuration": [ + { + "Identity": "Global", + "AllowEmailIntoChannel": true + }, + { + "Identity": "Tag:AllOn", + "AllowEmailIntoChannel": true + } + ], + "teams_tenant_info": [ + { + "ServiceInstance": "MicrosoftCommunicationsOnline/GOV-1B-G6" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "N/A: Feature is unavailable in GCC environments" +} diff --git a/Testing/Unit/Rego/Teams/TeamsConfig2_08_test.rego b/Testing/Unit/Rego/Teams/TeamsConfig2_08_test.rego new file mode 100644 index 0000000000..7c40b2efc0 --- /dev/null +++ b/Testing/Unit/Rego/Teams/TeamsConfig2_08_test.rego @@ -0,0 +1,355 @@ +package teams +import future.keywords + + +# +# Policy 1 +#-- +test_DefaultCatalogAppsType_Correct_V1 if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD allow all apps published by Microsoft, but MAY block specific Microsoft apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Global", + "DefaultCatalogAppsType": "BlockedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_DefaultCatalogAppsType_Correct_V2 if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD allow all apps published by Microsoft, but MAY block specific Microsoft apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Tag:TestPolicy", + "DefaultCatalogAppsType": "BlockedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_DefaultCatalogAppsType_Incorrect_V1 if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD allow all apps published by Microsoft, but MAY block specific Microsoft apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Global", + "DefaultCatalogAppsType": "AllowedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) found that block Microsoft Apps by default: Global" +} + +test_DefaultCatalogAppsType_Incorrect_V2 if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD allow all apps published by Microsoft, but MAY block specific Microsoft apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Tag:TestPolicy", + "DefaultCatalogAppsType": "AllowedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) found that block Microsoft Apps by default: Tag:TestPolicy" +} + +test_DefaultCatalogAppsType_Multiple if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD allow all apps published by Microsoft, but MAY block specific Microsoft apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Global", + "DefaultCatalogAppsType": "BlockedAppList" + }, + { + "Identity": "Tag:TestPolicy1", + "DefaultCatalogAppsType": "AllowedAppList" + }, + { + "Identity": "Tag:TestPolicy2", + "DefaultCatalogAppsType": "AllowedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + startswith(RuleOutput[0].ReportDetails, "2 meeting policy(ies) found that block Microsoft Apps by default: ") + contains(RuleOutput[0].ReportDetails, "Tag:TestPolicy1") + contains(RuleOutput[0].ReportDetails, "Tag:TestPolicy2") +} + +# +# Policy 2 +#-- +test_GlobalCatalogAppsType_Correct_V1 if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD NOT allow installation of all third-party apps, but MAY allow specific apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Global", + "GlobalCatalogAppsType": "AllowedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_GlobalCatalogAppsType_Correct_V2 if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD NOT allow installation of all third-party apps, but MAY allow specific apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Tag:TestPolicy", + "GlobalCatalogAppsType": "AllowedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_GlobalCatalogAppsType_Incorrect_V1 if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD NOT allow installation of all third-party apps, but MAY allow specific apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Global", + "GlobalCatalogAppsType": "BlockedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) found that allow third-party apps by default: Global" +} + +test_GlobalCatalogAppsType_Incorrect_V2 if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD NOT allow installation of all third-party apps, but MAY allow specific apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Tag:TestPolicy", + "GlobalCatalogAppsType": "BlockedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) found that allow third-party apps by default: Tag:TestPolicy" +} + +test_GlobalCatalogAppsType_Multiple if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD NOT allow installation of all third-party apps, but MAY allow specific apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Global", + "GlobalCatalogAppsType": "BlockedAppList" + }, + { + "Identity": "Tag:TestPolicy1", + "GlobalCatalogAppsType": "AllowedAppList" + }, + { + "Identity": "Tag:TestPolicy2", + "GlobalCatalogAppsType": "BlockedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + startswith(RuleOutput[0].ReportDetails, "2 meeting policy(ies) found that allow third-party apps by default: ") + contains(RuleOutput[0].ReportDetails, "Global") + contains(RuleOutput[0].ReportDetails, "Tag:TestPolicy2") +} + +test_PrivateCatalogAppsType_Correct_V1 if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD NOT allow installation of all custom apps, but MAY allow specific apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Global", + "PrivateCatalogAppsType": "AllowedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_PrivateCatalogAppsType_Correct_V2 if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD NOT allow installation of all custom apps, but MAY allow specific apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Tag:TestPolicy", + "PrivateCatalogAppsType": "AllowedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_PrivateCatalogAppsType_Incorrect_V1 if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD NOT allow installation of all custom apps, but MAY allow specific apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Global", + "PrivateCatalogAppsType": "BlockedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) found that allow custom apps by default: Global" +} + +test_PrivateCatalogAppsType_Incorrect_V2 if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD NOT allow installation of all custom apps, but MAY allow specific apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Tag:TestPolicy", + "PrivateCatalogAppsType": "BlockedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) found that allow custom apps by default: Tag:TestPolicy" +} + +test_PrivateCatalogAppsType_Multiple if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD NOT allow installation of all custom apps, but MAY allow specific apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Global", + "PrivateCatalogAppsType": "BlockedAppList" + }, + { + "Identity": "Tag:TestPolicy1", + "PrivateCatalogAppsType": "AllowedAppList" + }, + { + "Identity": "Tag:TestPolicy2", + "PrivateCatalogAppsType": "BlockedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + startswith(RuleOutput[0].ReportDetails, "2 meeting policy(ies) found that allow custom apps by default: ") + contains(RuleOutput[0].ReportDetails, "Global") + contains(RuleOutput[0].ReportDetails, "Tag:TestPolicy2") +} + +# +# Policy 3 +#-- +test_3rdParty_Correct if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHALL establish policy dictating the app review and approval process to be used by the agency" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Cannot be checked automatically. See Microsoft Teams Secure Configuration Baseline policy 2.8 for instructions on manual check" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/Teams/TeamsConfig2_09_test.rego b/Testing/Unit/Rego/Teams/TeamsConfig2_09_test.rego new file mode 100644 index 0000000000..2513b670aa --- /dev/null +++ b/Testing/Unit/Rego/Teams/TeamsConfig2_09_test.rego @@ -0,0 +1,165 @@ +package teams +import future.keywords + + +# +# Policy 1 +#-- +test_AllowCloudRecording_Correct if { + ControlNumber := "Teams 2.9" + Requirement := "Cloud video recording SHOULD be disabled in the global (org-wide default) meeting policy" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowCloudRecording": false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowCloudRecording_Incorrect if { + ControlNumber := "Teams 2.9" + Requirement := "Cloud video recording SHOULD be disabled in the global (org-wide default) meeting policy" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowCloudRecording": true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_AllowCloudRecording_Multiple if { + ControlNumber := "Teams 2.9" + Requirement := "Cloud video recording SHOULD be disabled in the global (org-wide default) meeting policy" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowCloudRecording": false + }, + { + "Identity": "Tag:TestPolicy", + "AllowCloudRecording": true # This baseline only applies to the Global policy, + # so no failure will be produced for the non-global policies + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +# +# Policy 2 +#-- +test_AllowCloudRecording_Correct_V1 if { + ControlNumber := "Teams 2.9" + Requirement := "For all meeting polices that allow cloud recording, recordings SHOULD be stored inside the country of that agency's tenant" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowCloudRecording": false, + "AllowRecordingStorageOutsideRegion": false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowCloudRecording_Correct_V2 if { + ControlNumber := "Teams 2.9" + Requirement := "For all meeting polices that allow cloud recording, recordings SHOULD be stored inside the country of that agency's tenant" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowCloudRecording": false, + "AllowRecordingStorageOutsideRegion": true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowCloudRecording_Incorrect if { + ControlNumber := "Teams 2.9" + Requirement := "For all meeting polices that allow cloud recording, recordings SHOULD be stored inside the country of that agency's tenant" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowCloudRecording": true, + "AllowRecordingStorageOutsideRegion": true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) found that allow cloud recording and storage outside of the tenant's region: Global" +} + +test_AllowCloudRecording_Multiple if { + ControlNumber := "Teams 2.9" + Requirement := "For all meeting polices that allow cloud recording, recordings SHOULD be stored inside the country of that agency's tenant" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowCloudRecording": true, + "AllowRecordingStorageOutsideRegion": true + }, + { + "Identity": "Tag:custom", + "AllowCloudRecording": true, + "AllowRecordingStorageOutsideRegion": true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + startswith(RuleOutput[0].ReportDetails, "2 meeting policy(ies) found that allow cloud recording and storage outside of the tenant's region: ") + contains(RuleOutput[0].ReportDetails, "Global") + contains(RuleOutput[0].ReportDetails, "Tag:custom") +} \ No newline at end of file diff --git a/Testing/Unit/Rego/Teams/TeamsConfig2_10_test.rego b/Testing/Unit/Rego/Teams/TeamsConfig2_10_test.rego new file mode 100644 index 0000000000..6b94ff69bf --- /dev/null +++ b/Testing/Unit/Rego/Teams/TeamsConfig2_10_test.rego @@ -0,0 +1,71 @@ +package teams +import future.keywords + + +# +# Policy 1 +#-- +test_BroadcastRecordingMode_Correct if { + ControlNumber := "Teams 2.10" + Requirement := "Record an event SHOULD be set to Organizer can record" + + Output := tests with input as { + "broadcast_policies": [ + { + "Identity": "Global", + "BroadcastRecordingMode": "UserOverride" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_BroadcastRecordingMode_Incorrect if { + ControlNumber := "Teams 2.10" + Requirement := "Record an event SHOULD be set to Organizer can record" + + Output := tests with input as { + "broadcast_policies": [ + { + "Identity": "Global", + "BroadcastRecordingMode": "AlwaysRecord" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + + +test_BroadcastRecordingMode_Multiple if { + ControlNumber := "Teams 2.10" + Requirement := "Record an event SHOULD be set to Organizer can record" + + Output := tests with input as { + "broadcast_policies": [ + { + "Identity": "Global", + "BroadcastRecordingMode": "UserOverride" + }, + { + "Identity": "Tag:TestPolicy", # Should be ignored + "BroadcastRecordingMode": "AlwaysRecord" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/Teams/TeamsConfig2_11_test.rego b/Testing/Unit/Rego/Teams/TeamsConfig2_11_test.rego new file mode 100644 index 0000000000..16ba4a1f9f --- /dev/null +++ b/Testing/Unit/Rego/Teams/TeamsConfig2_11_test.rego @@ -0,0 +1,51 @@ +package teams +import future.keywords + + +# +# Policy 1 +#-- +test_3rdParty_Correct_V1 if { + ControlNumber := "Teams 2.11" + Requirement := "A DLP solution SHALL be enabled" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} + +# +# Policy 2 +#-- +test_3rdParty_Correct_V2 if { + ControlNumber := "Teams 2.11" + Requirement := "Agencies SHOULD use either the native DLP solution offered by Microsoft or a DLP solution that offers comparable services" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} + +# +# Policy 3 +#-- +test_3rdParty_Correct_V3 if { + ControlNumber := "Teams 2.11" + Requirement := "The DLP solution SHALL protect Personally Identifiable Information (PII) and sensitive information, as defined by the agency. At a minimum, the sharing of credit card numbers, taxpayer Identification Numbers (TIN), and Social Security Numbers (SSN) via email SHALL be restricted" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/Teams/TeamsConfig2_12_test.rego b/Testing/Unit/Rego/Teams/TeamsConfig2_12_test.rego new file mode 100644 index 0000000000..3a9262c9af --- /dev/null +++ b/Testing/Unit/Rego/Teams/TeamsConfig2_12_test.rego @@ -0,0 +1,35 @@ +package teams +import future.keywords + + +# +# Policy 1 +#-- +test_3rdParty_Correct_V1 if { + ControlNumber := "Teams 2.12" + Requirement := "Attachments included with Teams messages SHOULD be scanned for malware" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} + +# +# Policy 2 +#-- +test_3rdParty_Correct_V2 if { + ControlNumber := "Teams 2.12" + Requirement := "Users SHOULD be prevented from opening or downloading files detected as malware" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} \ No newline at end of file diff --git a/Testing/Unit/Rego/Teams/TeamsConfig2_13_test.rego b/Testing/Unit/Rego/Teams/TeamsConfig2_13_test.rego new file mode 100644 index 0000000000..b1da9e5446 --- /dev/null +++ b/Testing/Unit/Rego/Teams/TeamsConfig2_13_test.rego @@ -0,0 +1,51 @@ +package teams +import future.keywords + + +# +# Policy 1 +#-- +test_3rdParty_Correct_V1 if { + ControlNumber := "Teams 2.13" + Requirement := "URL comparison with a block-list SHOULD be enabled" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} + +# +# Policy 2 +#-- +test_3rdParty_Correct_V2 if { + ControlNumber := "Teams 2.13" + Requirement := "Direct download links SHOULD be scanned for malware" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} + +# +# Policy 3 +#-- +test_3rdParty_Correct_V3 if { + ControlNumber := "Teams 2.13" + Requirement := "User click tracking SHOULD be enabled" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check" +} \ No newline at end of file diff --git a/images/scuba-architecture.png b/images/scuba-architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..6b4f663a4a4128bb025f12f4aeafe401c78a3393 GIT binary patch literal 178045 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 z