From 3d336bcdb59f7ff57cb42de125cd19a14b194819 Mon Sep 17 00:00:00 2001 From: Ted Kolovos <107076927+tkol2022@users.noreply.github.com> Date: Fri, 15 Nov 2024 09:25:17 -0500 Subject: [PATCH 01/11] improve performance query to get count of users without advanced auditing enabled (#1406) --- .../ScubaGear/Modules/Providers/ExportDefenderProvider.psm1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PowerShell/ScubaGear/Modules/Providers/ExportDefenderProvider.psm1 b/PowerShell/ScubaGear/Modules/Providers/ExportDefenderProvider.psm1 index dd0fc6da00..7a6a0ffa68 100644 --- a/PowerShell/ScubaGear/Modules/Providers/ExportDefenderProvider.psm1 +++ b/PowerShell/ScubaGear/Modules/Providers/ExportDefenderProvider.psm1 @@ -143,13 +143,13 @@ function Export-DefenderProvider { $DLPLicense = ConvertTo-Json $false } - # Run commands to get count of users with Advanced auditing enabled + # Get count of users without Advanced auditing enabled # GUID below is service plan ID for M365_ADVANCED_AUDITING as defined # on Microsoft Licensing Reference shown here: # https://learn.microsoft.com/en-us/entra/identity/users/licensing-service-plan-reference $UserParameters = @{ConsistencyLevel = 'eventual' Count = 'UsersWithoutAdvancedAuditCount' - All = $true + Top = 1 Filter = "not assignedPlans/any(a:a/servicePlanId eq 2f442157-a11c-46b9-ae5b-6e39ff4e5849 and a/capabilityStatus eq 'Enabled')" } From fb64ab5afea6595e865da66ade09313da0c4214c Mon Sep 17 00:00:00 2001 From: James Garriss <52328727+james-garriss@users.noreply.github.com> Date: Fri, 15 Nov 2024 09:25:55 -0500 Subject: [PATCH 02/11] Correct omission in documentation about importing module when downloading from GitHub (#1412) * add import * remove cloning * fix -name error * Update docs/execution/execution.md Co-authored-by: Addam Schroll <108814318+schrolla@users.noreply.github.com> * Update docs/installation/github.md Co-authored-by: Addam Schroll <108814318+schrolla@users.noreply.github.com> --------- Co-authored-by: Addam Schroll <108814318+schrolla@users.noreply.github.com> --- docs/execution/execution.md | 4 +++- docs/installation/github.md | 13 ++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/execution/execution.md b/docs/execution/execution.md index 152aa7a726..226b5fd99e 100644 --- a/docs/execution/execution.md +++ b/docs/execution/execution.md @@ -8,9 +8,11 @@ If ScubaGear was installed by [downloading from GitHub](../installation/github.m ```powershell # Import the module into the session -Import-Module -Name .\PowerShell\ScubaGear +Import-Module .\PowerShell\ScubaGear ``` +> **Note**: Do not add a \ to the end of the `.\PowerShell\ScubaGear` path. + ## Install Dependencies ScubaGear requires a number of dependencies to be installed before it can be invoked. To install the dependencies: diff --git a/docs/installation/github.md b/docs/installation/github.md index 4299138fe1..79b9f08392 100644 --- a/docs/installation/github.md +++ b/docs/installation/github.md @@ -6,7 +6,18 @@ The recommended way to install ScubaGear is from [PSGallery](psgallery.md), but 2. Under the `Assets` header, click `ScubaGear-v1.4.0.zip`to download the zip file. 3. Extract the zip file into the folder of your choice. -Once ScubaGear has been downloaded, the required [dependencies](../prerequisites/dependencies.md) can be installed. +## Import Module + +When ScubaGear is installed by downloading from GitHub, it must be imported into every new PowerShell terminal session before it can be executed. To import the module, open a PowerShell 5.1 terminal, navigate to the repository folder, and run this command: + +```powershell +# Import the module into the session +Import-Module .\PowerShell\ScubaGear +``` + +> **Note**: Do not add a \ to the end of the `.\PowerShell\ScubaGear` path. + +Once ScubaGear has been downloaded and imported, the required [dependencies](../prerequisites/dependencies.md) can be installed. ## PowerShell Execution Policy From 0fd54016b7f9f51260cbc2e9946749007c3ffe4e Mon Sep 17 00:00:00 2001 From: James Garriss <52328727+james-garriss@users.noreply.github.com> Date: Fri, 15 Nov 2024 13:10:02 -0500 Subject: [PATCH 03/11] Update schedule for functional test workflow (#1428) * update cron schedule * fix spelling --- .github/workflows/test_production_function.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test_production_function.yaml b/.github/workflows/test_production_function.yaml index 646503aacd..dc95481a35 100644 --- a/.github/workflows/test_production_function.yaml +++ b/.github/workflows/test_production_function.yaml @@ -7,10 +7,10 @@ name: Functional Tests on: - # Run this workflow at 12:15 am - # on every Sun to Thr (b/c GMT -5) + # Run this workflow at 12:15 (or 1:15) am + # on every Mon to Fri (b/c GMT -5 or -4) schedule: - - cron: "15 4 * * 0-4" + - cron: "15 6 * * 1-5" # in GMT workflow_dispatch: inputs: # checkov:skip=CKV_GHA_7:Manual inputs are desired. From 961a74d457e5620d61e999d34eb4aaf78163cd5b Mon Sep 17 00:00:00 2001 From: Beth Haines Date: Wed, 20 Nov 2024 18:17:06 -0500 Subject: [PATCH 04/11] Add service principal setup to functional testing documentation (#1423) * amended Testing Readme with sections for issue links * changed titles of additional markdown docs for consistency * changed content to conform to Content Guide * changed titles of additional markdown docs for consistency * tweaks in titles and file names * refined titles for documents * fixed name of link --- Testing/Readme.md | 20 ++++---- Testing/docs/aad-service-principal.md | 71 +++++++++++++++++++++++++++ Testing/docs/aad-tenant.md | 13 +++++ 3 files changed, 93 insertions(+), 11 deletions(-) create mode 100644 Testing/docs/aad-service-principal.md create mode 100644 Testing/docs/aad-tenant.md diff --git a/Testing/Readme.md b/Testing/Readme.md index 8c21aa034e..610de5d612 100644 --- a/Testing/Readme.md +++ b/Testing/Readme.md @@ -3,7 +3,7 @@ ScubaGear repository consists of an automation suite to help test the functional This README outlines the ScubaGear software test automation and its usage. The document also contains instructions for adding new functional tests to existing automation suite. -## Table of Contents +## Table of Contents - [Functional Testing Prerequisites](#functional-testing-prerequisites) - [Windows System with Chrome](#windows-system-with-chrome) @@ -22,10 +22,10 @@ This README outlines the ScubaGear software test automation and its usage. The d - [Nightly Functional Tests](#nightly-functional-tests) - [Troubleshooting](#troubleshooting) - [Issues with installing Pester](#issues-with-installing-pester) - - [Iusses with Selenium](#iusses-with-selenium) + - [Issues with Selenium](#issues-with-selenium) - [Chrome browser version issue](#chrome-browser-version-issue) - [Service principal authentication issue](#service-principal-authentication-issue) - - [Additional resources for admins](#additional-resources-for-admins) + - [Setup Documentation for System Admins](#setup-documentation-for-system-admins) ## Functional Testing Prerequisites ## @@ -115,8 +115,7 @@ The optional variant can have values like: `g5`, `e5`, `gcc`, `pnp`, `spo`, `e#` After setting up the prerequisites, define the Pester test container parameters in a test execution utility script called "RunFunctionalTest.ps1" as shown below: - -``` +``` PowerShell $TestContainers = @() $TestContainers += New-PesterContainer -Path "Testing/Functional/Products" -Data @{ TenantDomain = "MyG5tenant.onmicrosoft.com"; TenantDisplayName = "My G5 tenant"; ProductName = "aad"; M365Environment = "gcc" } @@ -371,7 +370,7 @@ To Resolve above issue, try the following: ``` - Best practice is to install Pester and other modules as a non-admin user with either 'Current User' or 'AllUsers' scope. When modules are installed with 'AllUsers' scope, they are installed to the ``` $env:ProgramFiles\PowerShell\Modules``` location. The 'CurrentUser' scope installs modules to ``` $HOME\Documents\PowerShell\Modules``` location. -### Iusses with Selenium +### Issues with Selenium We have seen issues with Selenium where test orchestrator would not run and produce random errors. If you have seen issues where error messages does not match with any other items in this troubleshooting section, try uninstalling and re-installing Selenium using following commands: ``` uninstall-module Selenium @@ -416,10 +415,9 @@ If you are trying to run the test orchestrator as a service principal and your c ![service-principal-error](/images/service-principal.png) -### Additional resources for admins -The following resources are for M365 tenant admins to provide additional information on setting up the infrastructure (service principals, user provisioning, etc.) for functional testing of ScubaGear. - -- [How to setup the permissions required to execute the automated functional test orchestrator](https://github.com/cisagov/ScubaGear/issues/589) +### Setup Documentation for System Admins ### +The following resources are for M365 tenant admins to provide additional information on setting up the infrastructure (service principals, user provisioning, etc.) for functional testing of ScubaGear. -- [How to setup a tenant with the necessary AAD conditional access policies to run the Automated Functional Test Orchestrator](https://github.com/cisagov/ScubaGear/issues/591) +- How to [setup an AAD application](./docs/aad-service-principal.md) in the tenant using a service principal to run the automated functional test orchestrator +- How to [setup a tenant](./docs/aad-tenant.md) with the necessary AAD conditional access policies to run the automated functional test orchestrator diff --git a/Testing/docs/aad-service-principal.md b/Testing/docs/aad-service-principal.md new file mode 100644 index 0000000000..39a69f1e2e --- /dev/null +++ b/Testing/docs/aad-service-principal.md @@ -0,0 +1,71 @@ +# Setup an AAD app to Run the Functional Test Orchestrator with a Service Principal + +This section describes how to setup an AAD application in the tenant when you want to run the test orchestrator using a **service principal (non interactive login)**. Setup for user interactive login is documented in a separate section. + +Go to Azure AD > App Registrations and click New registration +![image](https://github.com/cisagov/ScubaGear/assets/107076927/ad9f7a2b-587b-4c06-b08a-8075e68c7df4) + +Enter the name "Scuba Functional Test Orchestrator" +Under Who can use this application or API select Single tenant +Click Register +![image](https://github.com/cisagov/ScubaGear/assets/107076927/835d9eff-911b-4f3c-beda-ca0c65286ead) +![image](https://github.com/cisagov/ScubaGear/assets/107076927/cbd602c0-998e-435a-b621-621aee0a9aff) + +Click on the API permissions page +Click Add a permission then select Microsoft Graph in the popup page. Note some of the permissions are not Graph so pay attention to special instructions below for those. +![image](https://github.com/cisagov/ScubaGear/assets/107076927/2640bf0b-4ebb-48a2-9f46-29f942f648fd) + +Select Application permissions and then add all of the required permissions in the list below which are required for AAD. Once you have selected all of the permissions, click the Add permissions button. + +## Microsoft Graph API permissions + +- Directory.Read.All +- GroupMember.Read.All +- Organization.Read.All +- Policy.Read.All +- RoleManagement.Read.Directory +- User.Read.All +- PrivilegedEligibilitySchedule.Read.AzureADGroup +- PrivilegedAccess.Read.AzureADGroup +- RoleManagementPolicy.Read.AzureADGroup + +## Office 365 Exchange Online API permissions (select from APIs my organization users) + +- Exchange.ManageAsApp (so the application can run cmdlets in Exchange Online) + +## Sharepoint API permissions (select from Microsoft APIs) + +- Sites.FullControl.All (so the application can update Sharepoint settings) + +![image](https://github.com/cisagov/ScubaGear/assets/107076927/998d4549-d31f-49a0-8d39-e75858dc8ae8) +![image](https://github.com/cisagov/ScubaGear/assets/107076927/8ead310d-4d66-4bab-a476-72e373c73cd1) +![image](https://github.com/cisagov/ScubaGear/assets/107076927/d51ccbc5-4c76-4989-9708-2a7b058e2244) +![image](https://github.com/cisagov/ScubaGear/assets/107076927/e4d2a461-6486-4666-970f-c94a24a5717d) +![image](https://github.com/cisagov/ScubaGear/assets/107076927/d6246581-483b-4cfb-8def-cdbc42589e36) +![image](https://github.com/cisagov/ScubaGear/assets/107076927/6d6081d3-b1a9-4d5b-abb1-41fa8ecc4005) + +Click the Grant admin consent button on the API permissions page and click Yes in the popup +![image](https://github.com/cisagov/ScubaGear/assets/107076927/f5bcf13d-1cc4-4fa6-8750-1d7059f0ec6b) + +The permissions page should now show that admin consent was granted for each of the permissions +![image](https://github.com/cisagov/ScubaGear/assets/107076927/6065fcba-f3c3-4a37-944f-f19c4c7e0e7a) + +## Assigning user roles to the Scuba Functional Test Orchestrator application + +Some of the products also need an AAD user role assigned to the application in order for it to be able to update the tenant settings when executing the functional tests. + +## Assign the following user roles to the "Scuba Functional Test Orchestrator" application using the AAD role assignments page. Note that these should be active assignments in tenants that include PIM + +- Exchange Administrator (for EXO and most of the Defender cmdlets except for the compliance ones) +- Compliance Data Administrator (for Defender since it uses Purview compliance center cmdlets such as Set-DlpCompliancePolicy, Set-DlpComplianceRule, Set-ProtectionAlert) +- Teams Administrator + +Here is an example screenshot that shows the service principal assigned to the Exchange Administrator role + +![image](https://github.com/cisagov/ScubaGear/assets/107076927/6b90524a-0888-4201-80b1-0216bec5a503) + +To complete the setup for PowerPlatform you must also execute the code below to register the service principal with Power Platform: + +``` PowerShell +Add-PowerAppsAccount -Endpoint prod -TenantID $tenantId # use -Endpoint usgov for gcc tenants +New-PowerAppManagementApp -ApplicationId $appId diff --git a/Testing/docs/aad-tenant.md b/Testing/docs/aad-tenant.md new file mode 100644 index 0000000000..d81b2a656c --- /dev/null +++ b/Testing/docs/aad-tenant.md @@ -0,0 +1,13 @@ +# Setup the Tenant for Functional Testing of AAD Conditional Access Policies + +Numerous test cases in the AAD functional test plan associated with conditional access rely on some dependencies in the tenant that you must setup ahead of time: + +- **Step 1** - Create a conditional access policy in the tenant named **Automated Test 1 - DO NOT MODIFY** and set it up as per the configuration in the screenshot below. Set the policy to **Report-only** (do NOT turn it on). This policy is downloaded from the tenant when the test orchestrator executes and its characteristics are modified in memory (using the test orchestrator's RunCached function) before the provider JSON is sent to the Rego. This CA policy must be configured exactly as shown below in order for the test cases to work correctly. The only purpose of this CA policy is to support the execution of the test plan - the policy serves no purpose to the actual operations of the tenant and hence why it must be set to Report-only. + +![image](https://github.com/cisagov/ScubaGear/assets/107076927/ae19af14-c551-4ef1-b98b-e2e32087bcb8) + +- **Step 2** - The following conditional access policies are also required to be created in the tenant and configure as per the respective AAD baseline policy instructions, including setting Enable Policy to **On**. The policy names in the tenant must be configure exactly as described below: + - MS.AAD.1.1v1 Legacy authentication SHALL be blocked + - MS.AAD.2.1v1 Users detected as high risk SHALL be blocked + - MS.AAD.2.3v1 Sign-ins detected as high risk SHALL be blocked + - MS.AAD.3.2v1 If phishing-resistant MFA has not been enforced, an alternative MFA method SHALL be enforced for all users From a6b2cbed4b7558ebf4e56077e9a4c5339f7d742d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 21 Nov 2024 15:42:49 -0800 Subject: [PATCH 05/11] Bump OPA version from v0.69.0 to v0.70.0 (#1395) Co-authored-by: GitHub Action --- PowerShell/ScubaGear/Modules/ScubaConfig/ScubaConfig.psm1 | 2 +- PowerShell/ScubaGear/Modules/Support/Support.psm1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/PowerShell/ScubaGear/Modules/ScubaConfig/ScubaConfig.psm1 b/PowerShell/ScubaGear/Modules/ScubaConfig/ScubaConfig.psm1 index 87a3391e04..5da53446b2 100644 --- a/PowerShell/ScubaGear/Modules/ScubaConfig/ScubaConfig.psm1 +++ b/PowerShell/ScubaGear/Modules/ScubaConfig/ScubaConfig.psm1 @@ -40,7 +40,7 @@ class ScubaConfig { "Hybrid Identity Administrator", "Application Administrator", "Cloud Application Administrator") - DefaultOPAVersion = '0.69.0' + DefaultOPAVersion = '0.70.0' } static [object]ScubaDefault ([string]$Name){ diff --git a/PowerShell/ScubaGear/Modules/Support/Support.psm1 b/PowerShell/ScubaGear/Modules/Support/Support.psm1 index 9ca7175e54..4c5b1f8532 100644 --- a/PowerShell/ScubaGear/Modules/Support/Support.psm1 +++ b/PowerShell/ScubaGear/Modules/Support/Support.psm1 @@ -250,7 +250,7 @@ function Install-OPAforSCuBA { ) # Constants - $ACCEPTABLEVERSIONS = [ScubaConfig]::ScubaDefault('DefaultOPAVersion') # End Versions + $ACCEPTABLEVERSIONS = '0.69.0', [ScubaConfig]::ScubaDefault('DefaultOPAVersion') # End Versions $FILENAME = @{ Windows = "opa_windows_amd64.exe"; MacOS = "opa_darwin_amd64"; Linux = "opa_linux_amd64_static"} # Set prefernces for writing messages From afc948d97a69506e20f66377f7a2e0d878031dd8 Mon Sep 17 00:00:00 2001 From: James Garriss <52328727+james-garriss@users.noreply.github.com> Date: Thu, 21 Nov 2024 19:58:01 -0500 Subject: [PATCH 06/11] Convert workflow testing code to functions (#1430) * Convert modules to functions * fix lint * source functions * fix source typo * fix repo path * fix source path for pester test --- .github/workflows/publish_private_package.yaml | 11 ++++++----- .../Initialize-ScubaGearForTesting.Tests.ps1 | 7 ++++--- .../workflow/Install-SeleniumForTesting.Tests.ps1 | 6 ++++-- ...ing.psm1 => Initialize-ScubaGearForTesting.ps1} | 14 +++++++------- ...Testing.psm1 => Install-SeleniumForTesting.ps1} | 14 +++++++------- 5 files changed, 28 insertions(+), 24 deletions(-) rename utils/workflow/{Initialize-ScubaGearForTesting.psm1 => Initialize-ScubaGearForTesting.ps1} (79%) rename utils/workflow/{Install-SeleniumForTesting.psm1 => Install-SeleniumForTesting.ps1} (79%) diff --git a/.github/workflows/publish_private_package.yaml b/.github/workflows/publish_private_package.yaml index e12422c867..6e92a4e323 100644 --- a/.github/workflows/publish_private_package.yaml +++ b/.github/workflows/publish_private_package.yaml @@ -96,18 +96,19 @@ jobs: } $Config = New-PesterConfiguration -Hashtable $PesterConfig Invoke-Pester -Configuration $Config - # This is a manual test that simply writes the version to the console. - - name: Print Scuba Version + - name: Initialize ScubaGear run: | - cd repo Install-Module -Name ScubaGear -Repository $env:GalleryName -SkipPublisherCheck - Import-Module -Name ./utils/workflow/Initialize-ScubaGearForTesting + # Source the function + . repo/utils/workflow/Initialize-ScubaGearForTesting.ps1 Initialize-ScubaGearForTesting + # This is a manual test that simply writes the version to the console. Invoke-SCuBA -Version Test-Path -Path "C:\Program Files\WindowsPowerShell\Modules\ScubaGear" - name: Install Selenium run: | - Import-Module -Name .\repo\utils\workflow\Install-SeleniumForTesting + # Source the function + . repo/utils/workflow/Install-SeleniumForTesting.ps1 Install-SeleniumForTesting # Instead of initializing ScubaGear repeated for each of the test jobs below, # we cache the PowerShell modules and OPA here and then restore them for the jobs. diff --git a/Testing/workflow/Initialize-ScubaGearForTesting.Tests.ps1 b/Testing/workflow/Initialize-ScubaGearForTesting.Tests.ps1 index 8c004e976c..1d8ba00fe5 100644 --- a/Testing/workflow/Initialize-ScubaGearForTesting.Tests.ps1 +++ b/Testing/workflow/Initialize-ScubaGearForTesting.Tests.ps1 @@ -6,8 +6,9 @@ param() BeforeDiscovery { - Import-Module -Name .\utils\workflow\Initialize-ScubaGearForTesting - Import-Module -Name .\PowerShell\ScubaGear + # Source the function + . $PSScriptRoot/../../utils/workflow/Initialize-ScubaGearForTesting.ps1 + # Initialize SG Initialize-ScubaGearForTesting } @@ -26,7 +27,7 @@ else { Write-Warning 'Did NOT find list of modules!!' } -Describe "Check for PowerShell modules" { +Describe "PowerShell Modules Check" { foreach ($Module in $ModuleList) { $global:ModuleName = $Module.ModuleName It "Module $global:moduleName should be installed" { diff --git a/Testing/workflow/Install-SeleniumForTesting.Tests.ps1 b/Testing/workflow/Install-SeleniumForTesting.Tests.ps1 index 388680fab8..ed79dbf6ea 100644 --- a/Testing/workflow/Install-SeleniumForTesting.Tests.ps1 +++ b/Testing/workflow/Install-SeleniumForTesting.Tests.ps1 @@ -1,11 +1,13 @@ # The purpose of this test is to verify that Selenium was installed. BeforeDiscovery { - Import-Module -Name .\utils\workflow\Install-SeleniumForTesting + # Source the function + . $PSScriptRoot/../../utils/workflow/Install-SeleniumForTesting.ps1 + # Install Selenium Install-SeleniumForTesting } -Describe "Check for Selenium" { +Describe "Selenium Check" { It "Selenium should be installed" { $module = Get-Module -ListAvailable -Name 'Selenium' $module | Should -Not -BeNullOrEmpty diff --git a/utils/workflow/Initialize-ScubaGearForTesting.psm1 b/utils/workflow/Initialize-ScubaGearForTesting.ps1 similarity index 79% rename from utils/workflow/Initialize-ScubaGearForTesting.psm1 rename to utils/workflow/Initialize-ScubaGearForTesting.ps1 index 1e0e5f02d1..dc585aab29 100644 --- a/utils/workflow/Initialize-ScubaGearForTesting.psm1 +++ b/utils/workflow/Initialize-ScubaGearForTesting.ps1 @@ -1,5 +1,9 @@ -function Initialize-ScubaGearForTesting -{ +function Initialize-ScubaGearForTesting { + <# + .DESCRIPTION + Initializes ScubaGear for testing + #> + Write-Output 'Initializing ScubaGear for testing...' $RepoRootPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..' -Resolve Write-Output 'The repo root path is' @@ -8,8 +12,4 @@ function Initialize-ScubaGearForTesting Import-Module (Join-Path -Path $RepoRootPath -ChildPath 'PowerShell/ScubaGear') -Function Initialize-Scuba Write-Output 'Calling Initialize ScubaGear...' Initialize-SCuBA -} - -Export-ModuleMember -Function @( - 'Initialize-ScubaGearForTesting' -) \ No newline at end of file +} \ No newline at end of file diff --git a/utils/workflow/Install-SeleniumForTesting.psm1 b/utils/workflow/Install-SeleniumForTesting.ps1 similarity index 79% rename from utils/workflow/Install-SeleniumForTesting.psm1 rename to utils/workflow/Install-SeleniumForTesting.ps1 index 00e25fde36..51b7978a32 100644 --- a/utils/workflow/Install-SeleniumForTesting.psm1 +++ b/utils/workflow/Install-SeleniumForTesting.ps1 @@ -1,5 +1,9 @@ -function Install-SeleniumForTesting -{ +function Install-SeleniumForTesting { + <# + .DESCRIPTION + Installs Selenium for ScubaGear testing + #> + Write-Output 'Installing Selenium for testing...' Install-Module -Name Selenium -Scope CurrentUser -Force Import-Module -Name Selenium @@ -7,8 +11,4 @@ function Install-SeleniumForTesting Join-Path -Path $RepoRootPath -ChildPath 'Testing/Functional/SmokeTest/UpdateSelenium.ps1' # Workaround for Selenium. Loading psm1 instead of psd1 Import-Module -Name (Get-Module -Name Selenium -ListAvailable).Path -Force -} - -Export-ModuleMember -Function @( - 'Install-SeleniumForTesting' -) \ No newline at end of file +} \ No newline at end of file From 77dc279d49971fc92b0588751a10b7f62ec11932 Mon Sep 17 00:00:00 2001 From: Alden Hilton <106177711+adhilto@users.noreply.github.com> Date: Fri, 22 Nov 2024 10:41:13 -0800 Subject: [PATCH 07/11] Add the report UUID to the ScubaResults.json filename (#1426) * Initial implementation of adding UUID to the file name * Add back missing ConvertFrom-Json call * Mock Get-ChildItem in unit tests * Document addition of UUID to ScubaResults file name * Add unit tests for when there are multiple ScubaResults*.json files * Correct minor typo in documentation * remove wildcard search in ConvertTo-ResultsCSV code path * add error handling of window path length limit errors * fix some of the tests * fix the all the current broken unit tests * additional unit tests * add back in accidentally removed fields in config * complete lorem ipsum * todo message * remove UUID truncation for now * first draft * add truncation param to documenatation * spacing * fix failing test cases; handle full truncation case * make code consistent; add code comments to describe it's purpose * review feedback; point to additional options in the error message * PR Review: Fix absolute path check; fix config file override * review feedback; move new parameter in alphabetical order in docs * keep documentation consistent * remove configuration paramset from scubacached * code comments for the new edge case * Remove OBE unit test * Remove duplicate word * fix typos, wording and formatting in config * Refactor truncation logic into own function * rm duplicate text from PowerShell as well * captialize * remove long path check let the set-content naturally error out * add long path error check within the catch block * remove todo * Add UUID to mock data for cached tests * Fix unit tests * Remove commented out validation code. Co-authored-by: mitchelbaker-cisa <149098823+mitchelbaker-cisa@users.noreply.github.com> * add validation set to check invalid config file parameter * Remove stacktrace --------- Co-authored-by: buidav <105074908+buidav@users.noreply.github.com> Co-authored-by: mitchelbaker-cisa <149098823+mitchelbaker-cisa@users.noreply.github.com> --- .../ScubaGear/Modules/Orchestrator.psm1 | 242 +++++++++++++----- .../Modules/ScubaConfig/ScubaConfig.psm1 | 5 + ...21189b0e-f045-43ee-b9ba-653b32744e45.json} | 4 +- .../ConvertTo-ResultsCsv.Tests.ps1 | 19 +- .../Get-FullOutJsonName.Tests.ps1 | 43 ++++ .../Invoke-ProviderList.Tests.ps1 | 13 +- .../Orchestrator/Invoke-ScubaCached.Tests.ps1 | 31 ++- .../Orchestrator/Merge-JsonOutput.Tests.ps1 | 36 +-- docs/configuration/parameters.md | 102 +++++--- docs/execution/reports.md | 2 +- 10 files changed, 364 insertions(+), 133 deletions(-) rename PowerShell/ScubaGear/Sample-Reports/{ScubaResults.json => ScubaResults_21189b0e-f045-43ee-b9ba-653b32744e45.json} (99%) create mode 100644 PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Get-FullOutJsonName.Tests.ps1 diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index f762231122..66f2cb4f03 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -71,7 +71,7 @@ function Invoke-SCuBA { This parameter is for backwards compatibility for those working with the older ScubaGear output files. .Parameter OutJsonFileName If KeepIndividualJSON is not set, the name of the consolidated json created in the folder - created in OutPath. Defaults to "ScubaResults". + created in OutPath. Defaults to "ScubaResults". The report UUID will be appended to this. .Parameter OutCsvFileName The CSV created in the folder created in OutPath that contains the CSV version of the test results. Defaults to "ScubaResults". @@ -90,11 +90,14 @@ function Invoke-SCuBA { Set switch to enable report dark mode by default. .Parameter Quiet Do not launch external browser for report. + .Parameter NumberOfUUIDCharactersToTruncate + Controls how many characters will be truncated from the report UUID when appended to the end of OutJsonFileName. + Valid values are 0, 13, 18, 36 .Example 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_*. + security baselines. The output will stored in the current directory in a folder called M365BaselineConformance_*. .Example Invoke-SCuBA -Version This example returns the version of SCuBAGear. @@ -254,10 +257,17 @@ function Invoke-SCuBA { [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')] [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [switch] - $Quiet + $Quiet, + + [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')] + [Parameter(Mandatory = $false, ParameterSetName = 'Report')] + [ValidateNotNullOrEmpty()] + [ValidateSet(0, 13, 18, 36)] + [int] + $NumberOfUUIDCharactersToTruncate = [ScubaConfig]::ScubaDefault('DefaultNumberOfUUIDCharactersToTruncate') ) process { - # Retrive ScubaGear Module versions + # Retrieve ScubaGear Module versions $ParentPath = Split-Path $PSScriptRoot -Parent -ErrorAction 'Stop' $ScubaManifest = Import-PowerShellDataFile (Join-Path -Path $ParentPath -ChildPath 'ScubaGear.psd1' -Resolve) -ErrorAction 'Stop' $ModuleVersion = $ScubaManifest.ModuleVersion @@ -290,6 +300,7 @@ function Invoke-SCuBA { 'OutJsonFileName' = $OutJsonFileName 'OutCsvFileName' = $OutCsvFileName 'OutActionPlanFileName' = $OutActionPlanFileName + 'NumberOfUUIDCharactersToTruncate' = $NumberOfUUIDCharactersToTruncate } $ScubaConfig = New-Object -Type PSObject -Property $ProvidedParameters @@ -374,22 +385,26 @@ function Invoke-SCuBA { # Tenant Metadata for the Report $TenantDetails = Get-TenantDetail -ProductNames $ScubaConfig.ProductNames -M365Environment $ScubaConfig.M365Environment + # Generate a GUID to uniquely identify the output JSON + $Guid = New-Guid -ErrorAction 'Stop' + try { # Provider Execution $ProviderParams = @{ - 'ProductNames' = $ScubaConfig.ProductNames; - 'M365Environment' = $ScubaConfig.M365Environment; - 'TenantDetails' = $TenantDetails; - 'ModuleVersion' = $ModuleVersion; - 'OutFolderPath' = $OutFolderPath; + 'ProductNames' = $ScubaConfig.ProductNames; + 'M365Environment' = $ScubaConfig.M365Environment; + 'TenantDetails' = $TenantDetails; + 'ModuleVersion' = $ModuleVersion; + 'OutFolderPath' = $OutFolderPath; 'OutProviderFileName' = $ScubaConfig.OutProviderFileName; - 'BoundParameters' = $PSBoundParameters; + 'Guid' = $Guid; + 'BoundParameters' = $PSBoundParameters; } $ProdProviderFailed = Invoke-ProviderList @ProviderParams if ($ProdProviderFailed.Count -gt 0) { $ScubaConfig.ProductNames = Compare-ProductList -ProductNames $ScubaConfig.ProductNames ` - -ProductsFailed $ProdProviderFailed ` - -ExceptionMessage 'All indicated Product Providers failed to execute' + -ProductsFailed $ProdProviderFailed ` + -ExceptionMessage 'All indicated Product Providers failed to execute' } # OPA Rego invocation @@ -424,24 +439,33 @@ function Invoke-SCuBA { } Invoke-ReportCreation @ReportParams + $FullNameParams = @{ + 'OutJsonFileName' = $ScubaConfig.OutJsonFileName; + 'Guid' = $Guid; + 'NumberOfUUIDCharactersToTruncate' = $ScubaConfig.NumberOfUUIDCharactersToTruncate; + } + $FullScubaResultsName = Get-FullOutJsonName @FullNameParams + if (-not $KeepIndividualJSON) { # Craft the complete json version of the output $JsonParams = @{ - 'ProductNames' = $ScubaConfig.ProductNames; - 'OutFolderPath' = $OutFolderPath; - 'OutProviderFileName' = $ScubaConfig.OutProviderFileName; - 'TenantDetails' = $TenantDetails; - 'ModuleVersion' = $ModuleVersion; - 'OutJsonFileName' = $ScubaConfig.OutJsonFileName; + 'ProductNames' = $ScubaConfig.ProductNames; + 'OutFolderPath' = $OutFolderPath; + 'OutProviderFileName' = $ScubaConfig.OutProviderFileName; + 'TenantDetails' = $TenantDetails; + 'ModuleVersion' = $ModuleVersion; + 'FullScubaResultsName' = $FullScubaResultsName; + 'Guid' = $Guid; } Merge-JsonOutput @JsonParams } + # Craft the csv version of just the results $CsvParams = @{ - 'ProductNames' = $ScubaConfig.ProductNames; - 'OutFolderPath' = $OutFolderPath; - 'OutJsonFileName' = $ScubaConfig.OutJsonFileName; - 'OutCsvFileName' = $ScubaConfig.OutCsvFileName; + 'ProductNames' = $ScubaConfig.ProductNames; + 'OutFolderPath' = $OutFolderPath; + 'FullScubaResultsName' = $FullScubaResultsName; + 'OutCsvFileName' = $ScubaConfig.OutCsvFileName; 'OutActionPlanFileName' = $ScubaConfig.OutActionPlanFileName; } ConvertTo-ResultsCsv @CsvParams @@ -540,6 +564,11 @@ function Invoke-ProviderList { [string] $OutProviderFileName, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $Guid, + [Parameter(Mandatory = $true)] [hashtable] $BoundParameters @@ -632,15 +661,6 @@ function Invoke-ProviderList { $ConfigDetails = "{}" } - try { - $Guid = New-Guid -ErrorAction 'Stop' - } - catch { - $Guid = "00000000-0000-0000-0000-000000000000" - $Warning = "Error generating new UUID. See the exception message for more details: $($_)" - Write-Warning $Warning - } - $BaselineSettingsExport = @" { "baseline_version": "1", @@ -851,6 +871,50 @@ function Format-PlainText { } } +function Get-FullOutJsonName { + <# + .Description + This function determines the full file name of the SCuBA results file. + .Functionality + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string] + $OutJsonFileName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $Guid, + + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [ValidateSet(0, 13, 18, 36)] + [int] + $NumberOfUUIDCharactersToTruncate + ) + process { + # Truncate the UUID at the end of the ScubaResults JSON file by the parameter value. + # This is is to possibly prevent Windows maximum path length errors that may occur when moving files + # with a large number of characters + $TruncatedGuid = $Guid.Substring(0, $Guid.Length - $NumberOfUUIDCharactersToTruncate) + + # If the UUID still exists after truncation + if ($TruncatedGuid.Length -gt 0) { + $ScubaResultsFileName = "$($OutJsonFileName)_$($TruncatedGuid).json" + } + else { + # Otherwise omit adding it to the resulting file name + $ScubaResultsFileName = "$($OutJsonFileName).json" + } + + $ScubaResultsFileName + } +} + function ConvertTo-ResultsCsv { <# .Description @@ -874,7 +938,7 @@ function ConvertTo-ResultsCsv { [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] - $OutJsonFileName, + $FullScubaResultsName, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] @@ -888,10 +952,11 @@ function ConvertTo-ResultsCsv { ) process { try { - $ScubaResultsFileName = Join-Path $OutFolderPath -ChildPath "$OutJsonFileName.json" - if (Test-Path $ScubaResultsFileName -PathType Leaf) { + $ScubaResultsPath = Join-Path $OutFolderPath -ChildPath $FullScubaResultsName + + if (Test-Path $ScubaResultsPath -PathType Leaf) { # The ScubaResults file exists, no need to look for the individual json files - $ScubaResults = Get-Content $ScubaResultsFileName | ConvertFrom-Json + $ScubaResults = Get-Content (Get-ChildItem $ScubaResultsPath).FullName | ConvertFrom-Json } else { # The ScubaResults file does not exists, so we need to look inside the IndividualReports @@ -992,7 +1057,12 @@ function Merge-JsonOutput { [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] - $OutJsonFileName + $FullScubaResultsName, + + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string] + $Guid ) process { try { @@ -1005,7 +1075,6 @@ function Merge-JsonOutput { $SettingsExport = Get-Content $SettingsExportPath -Raw $SettingsExportObject = $(ConvertFrom-Json $SettingsExport) $TimestampZulu = $SettingsExportObject.timestamp_zulu - $ReportUuid = $SettingsExportObject.report_uuid # Get a list and abbreviation mapping of the products assessed $FullNames = @() @@ -1029,7 +1098,7 @@ function Merge-JsonOutput { "Tool" = "ScubaGear"; "ToolVersion" = $ModuleVersion; "TimestampZulu" = $TimestampZulu; - "ReportUUID" = $ReportUuid; + "ReportUUID" = $Guid; } @@ -1070,9 +1139,8 @@ function Merge-JsonOutput { $ReportJson = $ReportJson.replace("\u003e", ">") $ReportJson = $ReportJson.replace("\u0027", "'") - # Save the file - $JsonFileName = Join-Path -Path $OutFolderPath "$($OutJsonFileName).json" -ErrorAction 'Stop' - $ReportJson | Set-Content -Path $JsonFileName -Encoding $(Get-FileEncoding) -ErrorAction 'Stop' + $ScubaResultsPath = Join-Path $OutFolderPath -ChildPath $FullScubaResultsName -ErrorAction 'Stop' + $ReportJson | Set-Content -Path $ScubaResultsPath -Encoding $(Get-FileEncoding) -ErrorAction 'Stop' # Delete the now redundant files foreach ($File in $DeletionList) { @@ -1080,9 +1148,19 @@ function Merge-JsonOutput { } } catch { - $MergeJsonErrorMessage = "Fatal Error involving the Json reports aggregation. ` - Ending ScubaGear execution. See the exception message for more details: $($_)" - throw $MergeJsonErrorMessage + if ($_.FullyQualifiedErrorId -eq "GetContentWriterPathTooLongError,Microsoft.PowerShell.Commands.SetContentCommand") { + $MAX_WINDOWS_PATH_LEN = 256 + $PathLengthErrorMessage = "ScubaGear was likely executed in a location where the maximum file path length is greater than the allowable Windows file system limit ` + Please execute ScubaGear in a directory where for Windows file path limit is less than $($MAX_WINDOWS_PATH_LEN).` + Another option is to change the -NumberOfUUIDCharactersToTruncate, -OutJSONFileName, or -OutFolderName parameters to achieve an acceptable file path length ` + See the Invoke-SCuBA parameters documentation for more details. $($_)" + throw $PathLengthErrorMessage + } + else { + $MergeJsonErrorMessage = "Fatal Error involving the Json reports aggregation. ` + Ending ScubaGear execution. See the exception message for more details: $($_)" + throw $MergeJsonErrorMessage + } } } } @@ -1598,7 +1676,7 @@ function Invoke-SCuBACached { This parameter is for backwards compatibility for those working with the older ScubaGear output files. .Parameter OutJsonFileName If KeepIndividualJSON is set, the name of the consolidated json created in the folder - created in OutPath. Defaults to "ScubaResults". + created in OutPath. Defaults to "ScubaResults". The report UUID will be appended to this. .Parameter OutCsvFileName The CSV created in the folder created in OutPath that contains the CSV version of the test results. Defaults to "ScubaResults". @@ -1607,6 +1685,9 @@ function Invoke-SCuBACached { SHALL controls with fields for documenting failure causes and remediation plans. Defaults to "ActionPlan". .Parameter DarkMode Set switch to enable report dark mode by default. + .Parameter NumberOfUUIDCharactersToTruncate + Controls how many characters will be truncated from the report UUID when appended to the end of OutJsonFileName. + Valid values are 0, 13, 18, 36 .Example Invoke-SCuBACached Run an assessment against by default a commercial M365 Tenant against the @@ -1728,7 +1809,13 @@ function Invoke-SCuBACached { [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [switch] - $DarkMode + $DarkMode, + + [Parameter(Mandatory = $false, ParameterSetName = 'Report')] + [ValidateNotNullOrEmpty()] + [ValidateSet(0, 13, 18, 36)] + [int] + $NumberOfUUIDCharactersToTruncate = [ScubaConfig]::ScubaDefault('DefaultNumberOfUUIDCharactersToTruncate') ) process { $ParentPath = Split-Path $PSScriptRoot -Parent @@ -1769,10 +1856,22 @@ function Invoke-SCuBACached { 'BoundParameters' = $PSBoundParameters; } - # Rego Testing failsafe + # Create a failsafe tenant metadata variable in case the + # provider cannot retrieve the data. $TenantDetails = @{"DisplayName"="Rego Testing";} $TenantDetails = $TenantDetails | ConvertTo-Json -Depth 3 + if ($ExportProvider) { + # Check if there is a previous ScubaResults file + # delete if found + $PreviousResultsFiles = Get-ChildItem -Path $OutPath -Filter "$($OutJsonFileName)*.json" + if ($PreviousResultsFiles) { + $PreviousResultsFiles | ForEach-Object { + Remove-Item $_.FullName -Force + } + } + + # authenticate $ProdAuthFailed = Invoke-Connection @ConnectionParams if ($ProdAuthFailed.Count -gt 0) { $Difference = Compare-Object $ProductNames -DifferenceObject $ProdAuthFailed -PassThru @@ -1784,6 +1883,10 @@ function Invoke-SCuBACached { } } $TenantDetails = Get-TenantDetail -ProductNames $ProductNames -M365Environment $M365Environment + + # A new GUID needs to be generated if the provider is run + $Guid = New-Guid -ErrorAction 'Stop' + $ProviderParams = @{ 'ProductNames' = $ProductNames; 'M365Environment' = $M365Environment; @@ -1791,6 +1894,7 @@ function Invoke-SCuBACached { 'ModuleVersion' = $ModuleVersion; 'OutFolderPath' = $OutFolderPath; 'OutProviderFileName' = $OutProviderFileName; + 'Guid' = $Guid; 'BoundParameters' = $PSBoundParameters; } Invoke-ProviderList @ProviderParams @@ -1802,8 +1906,13 @@ function Invoke-SCuBACached { # file depending on what version of ScubaGear created the output. If the provider output # does not exist as a stand-alone file, create it from the ScubaResults file so the other functions # can execute as normal. - $ScubaResultsFileName = Join-Path -Path $OutPath -ChildPath "$($OutJsonFileName).json" - $SettingsExport = $(Get-Content $ScubaResultsFileName | ConvertFrom-Json).Raw + $ScubaResultsFileName = Join-Path -Path $OutPath -ChildPath "$($OutJsonFileName)*.json" + # As there is the possibility that the wildcard will match multiple files, + # select the one that was created last if there are multiple. + # By default ScubaGear will output the files into their own folder. + # The only case this will happen is when someone personally moves multiple files into the + # same folder. + $SettingsExport = $(Get-Content (Get-ChildItem $ScubaResultsFileName | Sort-Object CreationTime -Descending | Select-Object -First 1).FullName | ConvertFrom-Json).Raw # Uses the custom UTF8 NoBOM function to reoutput the Provider JSON file $ProviderContent = $SettingsExport | ConvertTo-Json -Depth 20 @@ -1815,20 +1924,18 @@ function Invoke-SCuBACached { # Generate a new UUID if the original data doesn't have one if (-not (Get-Member -InputObject $SettingsExport -Name "report_uuid" -MemberType Properties)) { - try { - $Guid = New-Guid -ErrorAction 'Stop' - } - catch { - $Guid = "00000000-0000-0000-0000-000000000000" - $Warning = "Error generating new UUID. See the exception message for more details: $($_)" - Write-Warning $Warning - } + $Guid = New-Guid -ErrorAction 'Stop' $SettingsExport | Add-Member -Name 'report_uuid' -Value $Guid -Type NoteProperty - $ProviderContent = $SettingsExport | ConvertTo-Json -Depth 20 - $ActualSavedLocation = Set-Utf8NoBom -Content $ProviderContent ` - -Location $OutPath -FileName "$OutProviderFileName.json" - Write-Debug $ActualSavedLocation } + else { + # Otherwise grab the UUID from the JSON itself + $Guid = $SettingsExport.report_uuid + } + + $ProviderContent = $SettingsExport | ConvertTo-Json -Depth 20 + $ActualSavedLocation = Set-Utf8NoBom -Content $ProviderContent ` + -Location $OutPath -FileName "$OutProviderFileName.json" + Write-Debug $ActualSavedLocation $TenantDetails = $SettingsExport.tenant_details $RegoParams = @{ @@ -1853,6 +1960,13 @@ function Invoke-SCuBACached { Invoke-RunRego @RegoParams Invoke-ReportCreation @ReportParams + $FullNameParams = @{ + 'OutJsonFileName' = $OutJsonFileName; + 'Guid' = $Guid; + 'NumberOfUUIDCharactersToTruncate' = $NumberOfUUIDCharactersToTruncate; + } + $FullScubaResultsName = Get-FullOutJsonName @FullNameParams + if (-not $KeepIndividualJSON) { # Craft the complete json version of the output $JsonParams = @{ @@ -1861,7 +1975,8 @@ function Invoke-SCuBACached { 'OutProviderFileName' = $OutProviderFileName; 'TenantDetails' = $TenantDetails; 'ModuleVersion' = $ModuleVersion; - 'OutJsonFileName' = $OutJsonFileName; + 'FullScubaResultsName' = $FullScubaResultsName; + 'Guid' = $Guid; } Merge-JsonOutput @JsonParams } @@ -1869,12 +1984,11 @@ function Invoke-SCuBACached { $CsvParams = @{ 'ProductNames' = $ProductNames; 'OutFolderPath' = $OutFolderPath; - 'OutJsonFileName' = $OutJsonFileName; + 'FullScubaResultsName' = $FullScubaResultsName; 'OutCsvFileName' = $OutCsvFileName; 'OutActionPlanFileName' = $OutActionPlanFileName; } ConvertTo-ResultsCsv @CsvParams - } } diff --git a/PowerShell/ScubaGear/Modules/ScubaConfig/ScubaConfig.psm1 b/PowerShell/ScubaGear/Modules/ScubaConfig/ScubaConfig.psm1 index 5da53446b2..93c368e426 100644 --- a/PowerShell/ScubaGear/Modules/ScubaConfig/ScubaConfig.psm1 +++ b/PowerShell/ScubaGear/Modules/ScubaConfig/ScubaConfig.psm1 @@ -31,6 +31,7 @@ class ScubaConfig { DefaultOutJsonFileName = "ScubaResults" DefaultOutCsvFileName = "ScubaResults" DefaultOutActionPlanFileName = "ActionPlan" + DefaultNumberOfUUIDCharactersToTruncate = 18 DefaultPrivilegedRoles = @( "Global Administrator", "Privileged Role Administrator", @@ -156,6 +157,10 @@ class ScubaConfig { $this.Configuration.OutActionPlanFileName = [ScubaConfig]::ScubaDefault('DefaultOutActionPlanFileName') } + if (-Not $this.Configuration.NumberOfUUIDCharactersToTruncate){ + $this.Configuration.NumberOfUUIDCharactersToTruncate = [ScubaConfig]::ScubaDefault('DefaultNumberOfUUIDCharactersToTruncate') + } + return } diff --git a/PowerShell/ScubaGear/Sample-Reports/ScubaResults.json b/PowerShell/ScubaGear/Sample-Reports/ScubaResults_21189b0e-f045-43ee-b9ba-653b32744e45.json similarity index 99% rename from PowerShell/ScubaGear/Sample-Reports/ScubaResults.json rename to PowerShell/ScubaGear/Sample-Reports/ScubaResults_21189b0e-f045-43ee-b9ba-653b32744e45.json index dbe1d69282..2296d0a869 100644 --- a/PowerShell/ScubaGear/Sample-Reports/ScubaResults.json +++ b/PowerShell/ScubaGear/Sample-Reports/ScubaResults_21189b0e-f045-43ee-b9ba-653b32744e45.json @@ -22,7 +22,8 @@ }, "Tool": "ScubaGear", "ToolVersion": "1.4.0", - "TimestampZulu": "2024-08-02T19:25:11.166Z" + "TimestampZulu": "2024-08-02T19:25:11.166Z", + "ReportUUID": "21189b0e-f045-43ee-b9ba-653b32744e45" }, "Summary": { "AAD": { @@ -1339,6 +1340,7 @@ "module_version": "1.4.0", "date": "08/02/2024 14:25:11 Central Daylight Time", "timestamp_zulu": "2024-08-02T19:25:11.166Z", + "report_uuid": "21189b0e-f045-43ee-b9ba-653b32744e45", "tenant_details": [ { "AADAdditionalData": { diff --git a/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/ConvertTo-ResultsCsv.Tests.ps1 b/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/ConvertTo-ResultsCsv.Tests.ps1 index c9563a2557..3d1bf47fbd 100644 --- a/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/ConvertTo-ResultsCsv.Tests.ps1 +++ b/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/ConvertTo-ResultsCsv.Tests.ps1 @@ -15,6 +15,9 @@ InModuleScope Orchestrator { Mock -CommandName Get-FileEncoding Mock -CommandName ConvertTo-Csv { "" } Mock -CommandName Write-Warning {} + Mock -CommandName Get-ChildItem { + [pscustomobject]@{"FullName"="ScubaResults_00000000-0000-0000-0000-000000000000.json"; "CreationTime"=[DateTime]"2024-01-01"} + } } It 'Handles multiple products, control groups, and controls' { @@ -56,10 +59,10 @@ InModuleScope Orchestrator { }} } $CsvParameters = @{ - ProductNames = @("exo", "aad"); - OutFolderPath = "."; - OutJsonFileName = "ScubaResults"; - OutCsvFileName = "ScubaResults"; + ProductNames = @("exo", "aad"); + OutFolderPath = "."; + FullScubaResultsName = "ScubaResults"; + OutCsvFileName = "ScubaResults"; OutActionPlanFileName = "ActionPlan"; } { ConvertTo-ResultsCsv @CsvParameters} | Should -Not -Throw @@ -74,10 +77,10 @@ InModuleScope Orchestrator { Mock -CommandName ConvertFrom-Json {} Mock -CommandName Get-Content { throw "File not found" } $CsvParameters = @{ - ProductNames = @("exo", "aad"); - OutFolderPath = "."; - OutJsonFileName = "ScubaResults"; - OutCsvFileName = "ScubaResults"; + ProductNames = @("exo", "aad"); + OutFolderPath = "."; + FullScubaResultsName = "ScubaResults"; + OutCsvFileName = "ScubaResults"; OutActionPlanFileName = "ActionPlan"; } { ConvertTo-ResultsCsv @CsvParameters} | Should -Not -Throw diff --git a/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Get-FullOutJsonName.Tests.ps1 b/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Get-FullOutJsonName.Tests.ps1 new file mode 100644 index 0000000000..e75ce73b31 --- /dev/null +++ b/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Get-FullOutJsonName.Tests.ps1 @@ -0,0 +1,43 @@ +$OrchestratorPath = '../../../../Modules/Orchestrator.psm1' +Import-Module (Join-Path -Path $PSScriptRoot -ChildPath $OrchestratorPath) -Function 'Get-FullOutJsonName' + +Describe -Tag 'Orchestrator' -Name 'Get-FullOutJsonName' { + InModuleScope Orchestrator { + It 'Adds the full UUID' { + $FullNameParams = @{ + 'OutJsonFileName' = "ScubaResults"; + 'Guid' = "30ebce05-f8f0-4a09-8ec2-589efbbd0e72"; + 'NumberOfUUIDCharactersToTruncate' = 0; + } + (Get-FullOutJsonName @FullNameParams) | Should -eq "ScubaResults_30ebce05-f8f0-4a09-8ec2-589efbbd0e72.json" + } + It 'Handles partial truncation' { + $FullNameParams = @{ + 'OutJsonFileName' = "ScubaResults"; + 'Guid' = "30ebce05-f8f0-4a09-8ec2-589efbbd0e72"; + 'NumberOfUUIDCharactersToTruncate' = 18; + } + (Get-FullOutJsonName @FullNameParams) | Should -eq "ScubaResults_30ebce05-f8f0-4a09.json" + } + It 'Handles full truncation' { + $FullNameParams = @{ + 'OutJsonFileName' = "ScubaResults"; + 'Guid' = "30ebce05-f8f0-4a09-8ec2-589efbbd0e72"; + 'NumberOfUUIDCharactersToTruncate' = 36; + } + (Get-FullOutJsonName @FullNameParams) | Should -eq "ScubaResults.json" + } + It 'Handles non-default names' { + $FullNameParams = @{ + 'OutJsonFileName' = "my_results"; + 'Guid' = "30ebce05-f8f0-4a09-8ec2-589efbbd0e72"; + 'NumberOfUUIDCharactersToTruncate' = 18; + } + (Get-FullOutJsonName @FullNameParams) | Should -eq "my_results_30ebce05-f8f0-4a09.json" + } + } +} + +AfterAll { + Remove-Module Orchestrator -ErrorAction SilentlyContinue +} \ No newline at end of file diff --git a/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Invoke-ProviderList.Tests.ps1 b/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Invoke-ProviderList.Tests.ps1 index 5f745dc59a..edaa739e6d 100644 --- a/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Invoke-ProviderList.Tests.ps1 +++ b/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Invoke-ProviderList.Tests.ps1 @@ -30,12 +30,13 @@ Describe -Tag 'Orchestrator' -Name 'Invoke-ProviderList' { BeforeAll { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'ProviderParameters')] $ProviderParameters = @{ - OutFolderPath = "./output"; - OutProviderFileName = "ProviderSettingsExport"; - M365Environment = "commercial"; - TenantDetails = '{"DisplayName": "displayName"}'; - ModuleVersion = '1.0'; - BoundParameters = @{}; + OutFolderPath = "./output"; + OutProviderFileName = "ProviderSettingsExport"; + M365Environment = "commercial"; + TenantDetails = '{"DisplayName": "displayName"}'; + ModuleVersion = '1.0'; + BoundParameters = @{}; + Guid = "00000000-0000-0000-0000-000000000000" } } It 'With -ProductNames "aad", should not throw' { diff --git a/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Invoke-ScubaCached.Tests.ps1 b/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Invoke-ScubaCached.Tests.ps1 index 89e82742fe..c32a8d989f 100644 --- a/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Invoke-ScubaCached.Tests.ps1 +++ b/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Invoke-ScubaCached.Tests.ps1 @@ -27,9 +27,16 @@ InModuleScope Orchestrator { Mock -CommandName Write-Debug {} Mock -CommandName New-Item {} - Mock -CommandName Get-Content {} + Mock -CommandName Get-Content { "" } Mock -CommandName Get-Member { $true } Mock -CommandName New-Guid { "00000000-0000-0000-0000-000000000000" } + Mock -CommandName Get-ChildItem { + [pscustomobject]@{"FullName"="ScubaResults.json"; "CreationTime"=[DateTime]"2024-01-01"} + } + Mock -CommandName Remove-Item {} + Mock -CommandName ConvertFrom-Json { + [PSCustomObject]@{"report_uuid"="00000000-0000-0000-0000-000000000000"} + } } Context 'When checking the conformance of commercial tenants' { BeforeAll { @@ -126,6 +133,7 @@ InModuleScope Orchestrator { Should -Invoke -CommandName New-Guid -Exactly -Times 0 } It 'Given output without a UUID should generate a new one' { + Mock -CommandName ConvertFrom-Json { [PSCustomObject]@{} } Mock -CommandName Get-Member { $false } # Now Get-Member will return False so as far as the provider # can tell, the existing output does not have a UUID @@ -166,6 +174,27 @@ InModuleScope Orchestrator { {Invoke-SCuBACached @SplatParams} | Should -Throw } } + Context "When there are multiple ScubaResults*.json files" { + # It's possible (but not expected) that there are multiple files matching + # "ScubaResults*.json". In this case, ScubaGear should choose the file + # created most recently. + It 'Should select the most recently created' { + Mock -CommandName Get-ChildItem { @( + [pscustomobject]@{"FullName"="ScubaResultsOld.json"; "CreationTime"=[DateTime]"2023-01-01"}, + [pscustomobject]@{"FullName"="ScubaResultsNew.json"; "CreationTime"=[DateTime]"2024-01-01"}, + [pscustomobject]@{"FullName"="ScubaResultsOldest.json"; "CreationTime"=[DateTime]"2022-01-01"} + ) } + + Mock -CommandName Get-Content { + if ($Path -ne "ScubaResultsNew.json") { + # Should be the new one, throw if not + throw + } + } + + {Invoke-SCuBACached @SplatParams} | Should -Throw + } + } } } diff --git a/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Merge-JsonOutput.Tests.ps1 b/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Merge-JsonOutput.Tests.ps1 index 3cb7d9ef91..fb1dfdbe1e 100644 --- a/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Merge-JsonOutput.Tests.ps1 +++ b/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Merge-JsonOutput.Tests.ps1 @@ -4,16 +4,16 @@ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath $OrchestratorPath) -Func InModuleScope Orchestrator { Describe -Tag 'Orchestrator' -Name 'Merge-JsonOutput' { BeforeAll { - Mock -CommandName Join-Path { "." } Mock -CommandName Out-File {} Mock -CommandName Set-Content {} Mock -CommandName Remove-Item {} Mock -CommandName Get-Content { "" } Mock -CommandName ConvertFrom-Json { @{ - "ReportSummary"=@{"Date"=""} - "Results"=@(); - "timestamp_zulu"=""; - } + "ReportSummary" = @{"Date" = "" } + "Results" = @(); + "timestamp_zulu" = ""; + "report_uuid" = "00000000-0000-0000-0000-000000000000" + } } Mock -CommandName Add-Member {} Mock -CommandName ConvertTo-Json { "" } @@ -22,34 +22,38 @@ InModuleScope Orchestrator { BeforeAll { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'JsonParameters')] $JsonParameters = @{ - TenantDetails = @{"DisplayName" = "displayName"; "TenantId" = "tenantId"; "DomainName" = "domainName"}; - ModuleVersion = '1.0'; - OutFolderPath = "./" - OutProviderFileName = "ProviderSettingsExport" - OutJsonFileName = "ScubaResults" + TenantDetails = @{"DisplayName" = "displayName"; "TenantId" = "tenantId"; "DomainName" = "domainName" }; + ModuleVersion = '1.0'; + OutFolderPath = "./"; + OutProviderFileName = "ProviderSettingsExport"; + FullScubaResultsName = "ScubaResults.json"; + Guid = "00000000-0000-0000-0000-000000000000"; } } It 'Merge single result' { + Mock -CommandName Join-Path { "." } $JsonParameters += @{ - ProductNames = @("aad") + ProductNames = @("aad"); } - { Merge-JsonOutput @JsonParameters} | Should -Not -Throw + { Merge-JsonOutput @JsonParameters } | Should -Not -Throw Should -Invoke -CommandName ConvertFrom-Json -Exactly -Times 2 $JsonParameters.ProductNames = @() } It 'Merge multiple results' { + Mock -CommandName Join-Path { "." } $JsonParameters += @{ - ProductNames = @("aad", "teams") + ProductNames = @("aad", "teams"); } - { Merge-JsonOutput @JsonParameters} | Should -Not -Throw + { Merge-JsonOutput @JsonParameters } | Should -Not -Throw Should -Invoke -CommandName ConvertFrom-Json -Exactly -Times 3 $JsonParameters.ProductNames = @() } It 'Delete redundant files' { + Mock -CommandName Join-Path { "." } $JsonParameters += @{ - ProductNames = @("aad", "teams") + ProductNames = @("aad", "teams"); } - { Merge-JsonOutput @JsonParameters} | Should -Not -Throw + { Merge-JsonOutput @JsonParameters } | Should -Not -Throw Should -Invoke -CommandName Remove-Item -Exactly -Times 3 $JsonParameters.ProductNames = @() } diff --git a/docs/configuration/parameters.md b/docs/configuration/parameters.md index ed9fdbf1b0..d784b63beb 100644 --- a/docs/configuration/parameters.md +++ b/docs/configuration/parameters.md @@ -2,7 +2,7 @@ The `Invoke-SCuBA` cmdlet has several command-line parameters, which are described below. -> **Note**: Some parameters can also be specified in a [configuration file](configuration.md). If specified in both, command-line parameters have precedence over the config file. +> **Note**: Some parameters can also be specified in a [configuration file](configuration.md). If specified in both, command-line parameters have precedence over the config file. > **Note**: Parameters use the Pascal case convention, and their names are consistent with those in the configuration file. @@ -15,7 +15,7 @@ The `Invoke-SCuBA` cmdlet has several command-line parameters, which are describ | Optional | Yes | | Datatype | String | | Default | n/a | -| Config File | Yes | +| Config File | Yes | Here is an example using `-AppID`: @@ -27,7 +27,7 @@ Invoke-SCuBA -ProductNames teams ` -Organization contoso.onmicrosoft.com ``` -> **Note**: AppID, CertificateThumbprint, and Organization are part of a parameter set used for authentication; if one is specified, all three must be specified. +> **Note**: AppID, CertificateThumbprint, and Organization are part of a parameter set used for authentication; if one is specified, all three must be specified. ## CertificateThumbprint @@ -38,7 +38,7 @@ Invoke-SCuBA -ProductNames teams ` | Optional | Yes | | Datatype | String | | Default | n/a | -| Config File | Yes | +| Config File | Yes | Here is an example using `-CertificateThumbprint`: @@ -50,11 +50,11 @@ Invoke-SCuBA -ProductNames teams ` -Organization contoso.onmicrosoft.com ``` -> **Note**: AppID, CertificateThumbprint, and Organization are part of a parameter set used for authentication; if one is specified, all three must be specified. +> **Note**: AppID, CertificateThumbprint, and Organization are part of a parameter set used for authentication; if one is specified, all three must be specified. ## ConfigFilePath -**ConfigFilePath** is the path of a [configuration file](configuration.md) that ScubaGear parses for input parameters. +**ConfigFilePath** is the path of a [configuration file](configuration.md) that ScubaGear parses for input parameters. | Parameter | Value | |-------------|---------------------------------------| @@ -71,7 +71,7 @@ Invoke-SCuBA -ProductNames teams ` -ConfigFilePath C:\users\johndoe\Documents\scuba\config.json ``` -If `-ConfigFilePath` is specified, default values will be used for any parameters that are not added to the config file. These default values are shown in the [full config file](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/Sample-Config-Files/full_config.yaml). +If `-ConfigFilePath` is specified, default values will be used for any parameters that are not added to the config file. These default values are shown in the [full config file](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/Sample-Config-Files/full_config.yaml). More information about the configuration file can be found on the [configuration page](configuration.md). @@ -86,7 +86,7 @@ More information about the configuration file can be found on the [configuration | Optional | Yes | | Datatype | Switch | | Default | n/a | -| Config File | No | +| Config File | No | ```powershell # View the HTML report in dark mode @@ -103,7 +103,7 @@ Invoke-SCuBA -ProductNames teams ` | Optional | Yes | | Datatype | Switch | | Default | n/a | -| Config File | Yes | +| Config File | Yes | ```powershell # Delete the auth tokens @@ -111,6 +111,24 @@ Invoke-SCuBA -ProductNames teams ` -DisconnectOnExit ``` +## KeepIndividualJSON + +**KeepIndividualJSON** Keeps the individual JSON files (e.g., `TeamsReport.json`) in the `IndividualReports` folder along with `ProviderSettingsExport.json` without combining the results in to one uber JSON file named the `ScubaResults.json`. The parameter is for backwards compatibility with older versions of ScubaGear. + +| Parameter | Value | +|-------------|--------| +| Optional | Yes | +| Datatype | Switch | +| Default | n/a | +| Config File | No | + +```powershell +# Outputs legacy ScubaGear individual JSON output +Invoke-SCuBA -ProductNames teams ` + -KeepIndividualJSON +``` + + ## LogIn **LogIn** enforces or bypasses authentication. If `$true`, ScubaGear will prompt the user to provide credentials to establish a connection to the specified M365 products in the `ProductNames` variable. If `$false`, it will use the previously issued authentication token, if it has not expired. @@ -122,7 +140,7 @@ Invoke-SCuBA -ProductNames teams ` | Default | `$true` | | Config File | Yes | -This variable should typically be `$true`, as a connection is established in the current PowerShell terminal session with the first authentication. If another verification is run in the same PowerShell session, then this variable can be set to false to bypass a second authenticate. +This variable should typically be `$true`, as a connection is established in the current PowerShell terminal session with the first authentication. If another verification is run in the same PowerShell session, then this variable can be set to false to bypass a second authenticate. ```powershell # Reuse previous authentication @@ -141,7 +159,7 @@ Invoke-SCuBA -ProductNames teams ` | Optional | Yes | | Datatype | String | | Default | `commercial` | -| Config File | Yes | +| Config File | Yes | > **Note**: This parameter is required if authenticating to Power Platform. It is also required if executing the tool against GCC High or DoD tenants. @@ -160,21 +178,32 @@ The list of acceptable values are: | Government cloud tenants (high) | gcchigh | | Department of Defense tenants | dod | -## KeepIndividualJSON -**KeepIndividualJSON** Keeps the individual JSON files (e.g., `TeamsReport.json`) in the `IndividualReports` folder along with `ProviderSettingsExport.json` without combining the results in to one uber JSON file named the `ScubaResults.json`. The parameter is for backwards compatibility with older versions of ScubaGear. +## NumberOfUUIDCharactersToTruncate -| Parameter | Value | -|-------------|--------| -| Optional | Yes | -| Datatype | Switch | -| Default | n/a | -| Config File | No | +**NumberOfUUIDCharactersToTruncate** controls how many characters will be truncated from the report UUID when appended to the end of **OutJsonFileName**. + +| Parameter | Value | +|-------------|--------------------| +| Optional | Yes | +| Datatype | Integer | +| Default | 18 | +| Config File | Yes | + + +The list of acceptable values are: + +| Description | Value | +|----------------------------------------|------------| +| Do no truncation of the appended UUID | 0 | +| Remove one octet of the appended UUID | 13 | +| Remove two octets of the appended UUID | 18 | +| Remove the appended UUID completely | 36 | ```powershell -# Outputs legacy ScubaGear individual JSON output -Invoke-SCuBA -ProductNames teams ` - -KeepIndividualJSON +# Truncate the UUID at the end of OutJsonFileName by 18 characters +Invoke-SCuBA -ProductNames exo ` + -NumberOfUUIDCharactersToTruncate 18 ``` ## OPAPath @@ -205,7 +234,7 @@ Invoke-SCuBA -ProductNames teams ` | Optional | Yes | | Datatype | String | | Default | n/a | -| Config File | Yes | +| Config File | Yes | Here is an example using Organization: @@ -217,7 +246,7 @@ Invoke-SCuBA -ProductNames teams ` -Organization contoso.onmicrosoft.com ``` -> **Note**: AppID, CertificateThumbprint, and Organization are part of a parameter set used for authentication; if one is specified, all three must be specified. +> **Note**: AppID, CertificateThumbprint, and Organization are part of a parameter set used for authentication; if one is specified, all three must be specified. ## OutActionPlanFileName @@ -228,7 +257,7 @@ Invoke-SCuBA -ProductNames teams ` | Optional | Yes | | Datatype | String | | Default | `ActionPlan` | -| Config File | Yes | +| Config File | Yes | ```powershell @@ -246,7 +275,7 @@ Invoke-SCuBA -ProductNames teams ` | Optional | Yes | | Datatype | String | | Default | `ScubaResults` | -| Config File | Yes | +| Config File | Yes | ```powershell @@ -257,14 +286,14 @@ Invoke-SCuBA -ProductNames teams ` ## OutFolderName -**OutFolderName** is the first half of the name of the folder where the [report files](../execution/reports.md) will be created. The second half is a timedate stamp. The location of this folder is determined by the [OutPath](#outpath) parameter. +**OutFolderName** is the first half of the name of the folder where the [report files](../execution/reports.md) will be created. The second half is a timedate stamp. The location of this folder is determined by the [OutPath](#outpath) parameter. | Parameter | Value | |-------------|---------------------------| | Optional | Yes | | Datatype | String | | Default | `M365BaselineConformance` | -| Config File | Yes | +| Config File | Yes | ```powershell # Change the output folder @@ -274,14 +303,14 @@ Invoke-SCuBA -ProductNames teams ` ## OutJsonFileName -**OutJsonFileName** renames the uber output JSON file that is created after a ScubaGear run. This should only be the base file name, as the extension `.json` will automatically be added. +**OutJsonFileName** specifies the base file name of the uber output JSON file that is created after a ScubaGear run. This should only be the base file name; the report UUID as well as the extension, `.json`, will automatically be added. | Parameter | Value | |-------------|----------------| | Optional | Yes | | Datatype | String | | Default | `ScubaResults` | -| Config File | Yes | +| Config File | Yes | > **Note**: This parameter does not work if the `-KeepIndividualJSON` parameter is present. @@ -290,6 +319,7 @@ Invoke-SCuBA -ProductNames teams ` Invoke-SCuBA -ProductNames teams ` -OutJsonFileName myresults ``` +In the above example, the resulting JSON file name would be `myresults_21189b0e-f045-43ee-b9ba-653b32744e45.json` (substituting in the actual report UUID.) ## OutPath @@ -319,7 +349,7 @@ Invoke-SCuBA -ProductNames teams ` | Optional | Yes | | Datatype | String | | Default | `ProviderSettingsExport` | -| Config File | Yes | +| Config File | Yes | ```powershell # Change the provider settings file @@ -338,7 +368,7 @@ Invoke-SCuBA -ProductNames teams ` | Optional | Yes | | Datatype | String | | Default | `TestResults` | -| Config File | Yes | +| Config File | Yes | ```powershell # Change the rego file @@ -357,7 +387,7 @@ Invoke-SCuBA -ProductNames teams ` | Optional | Yes | | Datatype | String | | Default | `BaselineReports` | -| Config File | Yes | +| Config File | Yes | ```powershell # Change the HTML report file @@ -376,7 +406,7 @@ Invoke-SCuBA -ProductNames teams ` | Optional | Yes | | Datatype | List of Strings | | Default | ["aad", "defender", "exo", "sharepoint", "teams"] | -| Config File | Yes | +| Config File | Yes | The list of acceptable values are: @@ -405,7 +435,7 @@ Invoke-SCuBA -ProductNames teams, exo | Optional | Yes | | Datatype | Switch | | Default | n/a | -| Config File | No | +| Config File | No | ```powershell # Do not open the browser @@ -422,7 +452,7 @@ Invoke-SCuBA -ProductNames teams ` | Optional | Yes | | Datatype | Switch | | Default | n/a | -| Config File | No | +| Config File | No | ```powershell # Check the version diff --git a/docs/execution/reports.md b/docs/execution/reports.md index 1fc39b97da..700f9538c9 100644 --- a/docs/execution/reports.md +++ b/docs/execution/reports.md @@ -7,7 +7,7 @@ When ScubaGear runs, it creates a new time-stamped subdirectory wherein it will | `IndividualReports` | This directory contains the detailed reports for each product tested. | | `BaselineReports.html` | This HTML file is a summary of the detailed reports. By default, this file is automatically opened in a web browser after running ScubaGear. | | `ProvideSettingsExport.json` | This JSON file contains all of the information that ScubaGear extracted from the products. A highly-motivated admin might find this useful for understanding how ScubaGear arrived at its results. Only present if ScubaGear is run with the `KeepIndividualJson` flag; if run without the `KeepIndividualJSON` parameter, the contents of this file will be merged into the ScubaResults.json file. | -| `ScubaResults.json` | This JSON file encapsulates all ScubaGear output in a format that is automatically parsed by a downstream system. It contains metadata about the run and the tenant, summary counts of the test results, the test results, and the raw provider output. Not present if ScubaGear is run with the `KeepIndividualJSON` flag. | +| `ScubaResults_{UUID}.json` | This JSON file encapsulates all ScubaGear output in a format that is automatically parsed by a downstream system. It contains metadata about the run and the tenant, summary counts of the test results, the test results, and the raw provider output. Not present if ScubaGear is run with the `KeepIndividualJSON` flag. | | `ScubaResults.csv` | This CSV file contains the test results in a format that could be automatically parsed by a downstream system. Note that this CSV file only contains the results (i.e., the control ID, requirement string, etc.). It does not contain all data contained in the HTML or JSON versions of the output (i.e., the metadata, summary counts, or raw provider output) due to the limitations of CSV files. | | `ActionPlan.csv` | This CSV file contains the test results in a format that could be automatically parsed by a downstream system, filtered down to just failing "SHALL" controls. For each failing test, it includes fields where users can document reasons for failures and timelines for remediation, if they so choose. | From 4b681760f92d26fc9d1b5177c53b0624eeeb8305 Mon Sep 17 00:00:00 2001 From: Alden Hilton <106177711+adhilto@users.noreply.github.com> Date: Tue, 26 Nov 2024 13:51:45 -0800 Subject: [PATCH 08/11] Clarify License Requirements in assumptions.md (#1439) * Update assumptions.md license requirements language * Add Defender plan 2 --------- Co-authored-by: mitchelbaker-cisa <149098823+mitchelbaker-cisa@users.noreply.github.com> --- docs/misc/assumptions.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/misc/assumptions.md b/docs/misc/assumptions.md index e89dd43f77..fd572802a6 100644 --- a/docs/misc/assumptions.md +++ b/docs/misc/assumptions.md @@ -4,11 +4,10 @@ ScubaGear has been 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. +> **Note**: DOD endpoints are included, but have not been tested. Please open an issue if you encounter bugs. +Some of the policy checks in the baselines rely on the following licenses, which are included by default in M365 E5 and G5: * Microsoft Entra ID P2 -* Microsoft Defender for Office 365 Plan 1 +* Microsoft Defender for Office 365 Plan 1 or 2 -If a tenant does not have the licenses listed above, the report will display a non-compliant output for those policies. - -> **Note**: DOD endpoints are included, but have not been tested. Please open an issue if you encounter bugs. \ No newline at end of file +All controls that require a license beyond E3 or G3 note the required license in its respective "License Requirements" section. In almost all cases, the requirement for controls that require an additional license can be met using a third-party service. In order to prevent ScubaGear from reporting failures for any controls you implement using a third-party service, you will need to document that you use a third-party service by configuring ScubaGear to omit the relevant controls. See [Omit Policies](https://github.com/cisagov/ScubaGear/blob/main/docs/configuration/configuration.md#omit-policies) for more details. From e25a15b017e81917dab64c1877fa9beb4e0b8bfe Mon Sep 17 00:00:00 2001 From: David Bui <105074908+buidav@users.noreply.github.com> Date: Tue, 3 Dec 2024 06:37:19 -0800 Subject: [PATCH 09/11] Document ability to add organizational metadata and complying with SCuBA policy checks via configuration file (#1443) * Document scuba compliance yaml file * Additional documentation within the config file itself * Add link to Omit Policy documentation --- .../Sample-Config-Files/scuba_compliance.yaml | 80 +++++++++++++++++++ docs/configuration/configuration.md | 31 ++++++- 2 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 PowerShell/ScubaGear/Sample-Config-Files/scuba_compliance.yaml diff --git a/PowerShell/ScubaGear/Sample-Config-Files/scuba_compliance.yaml b/PowerShell/ScubaGear/Sample-Config-Files/scuba_compliance.yaml new file mode 100644 index 0000000000..1e235bf6d0 --- /dev/null +++ b/PowerShell/ScubaGear/Sample-Config-Files/scuba_compliance.yaml @@ -0,0 +1,80 @@ +Description: | + This is a ScubaGear sample configuration file for compliance use. + In order to pass/omit certain policy checks made during a ScubaGear assessment run, necessary action + is required to provide specific values unique to both your organization and M365 tenant in the fields below. + Please read and understand the full ScubaGear configuration file documentation for both adding additional fields or modifying the fields below. + The documentation can be found here: https://github.com/cisagov/ScubaGear/blob/main/docs/configuration/configuration.md + +# The list of M365 Product/SCuBA baselines ScubaGear will assess. +ProductNames: + - aad + - defender + - exo + - powerplatform + - sharepoint + - teams + +# This parameter is used to authenticate to the different commercial/government environments. +# See https://github.com/cisagov/ScubaGear/blob/main/docs/configuration/parameters.md#m365environment +# for list of acceptable values +M365Environment: commercial + +# The following fields are used to document what organization this configuration file belongs to. +OrgName: Department of Example +OrgUnitName: Subdepartment of Example + +# The OmitPolicy field can be used to exclude policies from the SCuBA assessment +# results (e.g., for controls that are met using a third party tool). +# See https://github.com/cisagov/ScubaGear/blob/main/docs/configuration/configuration.md#omit-policies +# for in depth details +OmitPolicy: {} + +# For product specific details on filling out the fields below, see: +# https://github.com/cisagov/ScubaGear/blob/main/docs/configuration/configuration.md#product-specific-configuration +# For specifics about YAML syntax: https://github.com/cisagov/ScubaGear/blob/main/docs/configuration/configuration.md#anchors-and-aliases +Aad: + MS.AAD.1.1v1: &OrgCapExclusions + CapExclusions: + Groups: + - "" + Users: + - "" + MS.AAD.2.1v1: *OrgCapExclusions + MS.AAD.2.3v1: *OrgCapExclusions + MS.AAD.3.1v1: *OrgCapExclusions + MS.AAD.3.2v1: *OrgCapExclusions + MS.AAD.3.3v1: *OrgCapExclusions + MS.AAD.3.6v1: *OrgCapExclusions + MS.AAD.3.7v1: *OrgCapExclusions + MS.AAD.3.8v1: *OrgCapExclusions + MS.AAD.7.4v1: + RoleExclusions: + Groups: + - "" + Users: + - "" +Defender: + MS.DEFENDER.1.4v1: &OrgSensitiveAccountFilter + SensitiveAccounts: + ExcludedUsers: + - "" + IncludedGroups: + - "" + IncludedDomains: + - "" + ExcludedDomains: + - "" + ExcludedGroups: + - "" + IncludedUsers: + - "" + MS.DEFENDER.1.5v1: *OrgSensitiveAccountFilter + MS.DEFENDER.2.1v1: + SensitiveUsers: + - "" + MS.DEFENDER.2.2v1: + AgencyDomains: + - "" + MS.DEFENDER.2.3v1: + PartnerDomains: + - "" diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md index 5d9606f93e..0410eaec5f 100644 --- a/docs/configuration/configuration.md +++ b/docs/configuration/configuration.md @@ -1,14 +1,18 @@ # ScubaGear Configuration File -Most of the `Invoke-SCuBA` cmdlet [parameters](parameters.md) can be placed into a configuration file in order to make execution easier. The path of the file is specified by the `-ConfigFilePath` parameter, and it contents can be formatted as YAML or JSON. +ScubaGear allows users to specify most of the `Invoke-SCuBA` cmdlet [parameters](parameters.md) in a configuration file. The path of the file is specified by the `-ConfigFilePath` parameter, and its contents can be formatted as YAML or JSON. Important details about executing ScubaGear with a configuration file are listed below. -> **Note**: If a parameter is also specified in a configuration file, the command-line parameter has precedence over the config file. +- Executing ScubaGear with a modified configuration file is required to pass or omit specific ScubaGear policy checks. See [SCuBA compliance use](https://github.com/cisagov/ScubaGear/blob/main/docs/configuration/configuration.md#scuba-compliance-use) and the associated sample configuration file for details. -> **Note**: The config files use the Pascal case convention for variables, and their names are consistent with the parameters. +- The configuration file allows users to add additional fields to embed within the ScubaGear output JSON for supplemental metadata purposes. + +- The configuration files use the Pascal case convention for variables, and their names are consistent with the parameters. + +> Important: When a parameter is specified on both the command line and the configuration file, the parameter value provided on the command line has precedence and the configuration file value will be disregarded. ## Sample Configuration Files -[Sample config files](https://github.com/cisagov/ScubaGear/tree/main/PowerShell/ScubaGear/Sample-Config-Files) are available in the repo. Four of these sample config files are explained in more detail in the sections below. +[Sample config files](https://github.com/cisagov/ScubaGear/tree/main/PowerShell/ScubaGear/Sample-Config-Files) are available in the repo. Several of these sample config files are explained in more detail in the sections below. ### Basic Use @@ -52,6 +56,25 @@ Invoke-SCuBA ` -CertificateThumbprint fedcba9876543210fedcba9876543210fedcba98 ``` +### SCuBA compliance use +The [SCuBA compliance](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/Sample-Config-Files/scuba_compliance.yaml) example config file contains a base essential set of parameters for organizations seeking to meet SCuBA compliance checks. +The configuration file contains a subset of ScubaGear parameters, fields for adding conditional access policy exceptions, fields for omitting ScubaGear policy checks, and additional fields for documenting the organization running ScubaGear. +Users are highly encouraged to read all the configuration file documentation sections to comprehend what each field is for and to modify those fields to successfully pass ScubaGear's SCuBA baseline compliance checks. + +Uniquely, this example configuration file contains the additional `OrgName` and `OrgUnitName` fields for documenting both the organization and organizational subunit owner of the M365 tenant ScubaGear is running against. + +``` +OrgName: Department of Example +OrgUnitName: Subdepartment of Example +``` + +ScubaGear can be invoked with this config file: + +```powershell +# Invoke with config file +Invoke-SCuBA -ConfigFilePath scuba_compliance.yaml +``` + ### Credential Use The [credential user](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/Sample-Config-Files/creds_config.yaml) example config file supplies credentials using a service principal, appId, and certificate thumbprint. (The associated private key is still required.) Config files with sensitive data should be protected appropriately. From 3ce71e9b4fcf9396305d073140c994739f015060 Mon Sep 17 00:00:00 2001 From: James Garriss <52328727+james-garriss@users.noreply.github.com> Date: Wed, 4 Dec 2024 15:24:25 -0500 Subject: [PATCH 10/11] Convert baseline markdown files to text and HTML (#1435) * add workflow * fix paths * test write permissions * add remaining baseline files --- .github/workflows/convert_baselines.yaml | 98 ++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 .github/workflows/convert_baselines.yaml diff --git a/.github/workflows/convert_baselines.yaml b/.github/workflows/convert_baselines.yaml new file mode 100644 index 0000000000..05ee5870b5 --- /dev/null +++ b/.github/workflows/convert_baselines.yaml @@ -0,0 +1,98 @@ +# Purpose: Converts the baseline Markdown files into HTML and text so that they can be ingested into Drupal. + +name: Convert Baseline to Text and HTML + +on: + workflow_dispatch: + push: + paths: + - ".github/workflows/convert_baselines.yaml" + +permissions: read-all + +jobs: + convert: + name: Convert + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + - name: Convert AAD to Text + uses: docker://pandoc/core:3.5 + with: + args: >- + --standalone + -f markdown + -t plain + PowerShell/ScubaGear/baselines/aad.md + --output=baselines/aad.txt + - name: Convert Defender to Text + uses: docker://pandoc/core:3.5 + with: + args: >- + --standalone + -f markdown + -t plain + PowerShell/ScubaGear/baselines/defender.md + --output=baselines/defender.txt + - name: Convert Exchange to Text + uses: docker://pandoc/core:3.5 + with: + args: >- + --standalone + -f markdown + -t plain + PowerShell/ScubaGear/baselines/exo.md + --output=baselines/exo.txt + - name: Convert Power BI to Text + uses: docker://pandoc/core:3.5 + with: + args: >- + --standalone + -f markdown + -t plain + PowerShell/ScubaGear/baselines/powerbi.md + --output=baselines/powerbi.txt + - name: Convert Power Platform to Text + uses: docker://pandoc/core:3.5 + with: + args: >- + --standalone + -f markdown + -t plain + PowerShell/ScubaGear/baselines/powerplatform.md + --output=baselines/powerflatform.txt + - name: Convert Sharepoint to Text + uses: docker://pandoc/core:3.5 + with: + args: >- + --standalone + -f markdown + -t plain + PowerShell/ScubaGear/baselines/sharepoint.md + --output=baselines/sharepoint.txt + - name: Convert Teams to Text + uses: docker://pandoc/core:3.5 + with: + args: >- + --standalone + -f markdown + -t plain + PowerShell/ScubaGear/baselines/teams.md + --output=baselines/teams.txt + - name: Install HTML converter + run: npm i markdown-to-html-cli -g + - name: Convert to HTML + run: | + markdown-to-html --source PowerShell/ScubaGear/baselines/aad.md --output baselines/aad.html + markdown-to-html --source PowerShell/ScubaGear/baselines/defender.md --output baselines/defender.html + markdown-to-html --source PowerShell/ScubaGear/baselines/exo.md --output baselines/exo.html + markdown-to-html --source PowerShell/ScubaGear/baselines/powerbi.md --output baselines/powerbi.html + markdown-to-html --source PowerShell/ScubaGear/baselines/powerplatform.md --output baselines/powerplatform.html + markdown-to-html --source PowerShell/ScubaGear/baselines/sharepoint.md --output baselines/sharepoint.html + markdown-to-html --source PowerShell/ScubaGear/baselines/teams.md --output baselines/teams.html + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: output + path: baselines From e1f201dddbe89a7a8d430cd66c0a338b00beb32d Mon Sep 17 00:00:00 2001 From: Addam Schroll <108814318+schrolla@users.noreply.github.com> Date: Mon, 9 Dec 2024 10:58:00 -0600 Subject: [PATCH 11/11] Remove extraneous SHALL from MS.DEFENDER.4.1 (#1408) * Clarify MS.DEFENDER.4.1v1 policy language to single shall * Update version in MS.DEFENDER.4.1 references in EXO baseline to v2 * Update MS.DEFENDER.4.1 policy ID version * Update MS.DEFENDER.4.1 policy id version in CreateReportStubs test results JSON output * Update MS.DEFENDER.4.1 policy id version in Defender rego unit tests * Increment policy ID version (MS.DEFENDER.4.1v1 -> v2) --- PowerShell/ScubaGear/Rego/DefenderConfig.rego | 4 +-- .../CreateReportStubs/TestResults.json | 10 +++--- .../Rego/Defender/DefenderConfig_04_test.rego | 20 +++++------ PowerShell/ScubaGear/baselines/defender.md | 26 +++++++------- PowerShell/ScubaGear/baselines/exo.md | 6 ++-- .../Products/TestPlans/defender.testplan.yaml | 34 +++++++++---------- 6 files changed, 50 insertions(+), 50 deletions(-) diff --git a/PowerShell/ScubaGear/Rego/DefenderConfig.rego b/PowerShell/ScubaGear/Rego/DefenderConfig.rego index f7f57044d9..d80e90c6fc 100644 --- a/PowerShell/ScubaGear/Rego/DefenderConfig.rego +++ b/PowerShell/ScubaGear/Rego/DefenderConfig.rego @@ -387,7 +387,7 @@ tests contains { ################# # -# MS.DEFENDER.4.1v1 +# MS.DEFENDER.4.1v2 #-- SensitiveContent := [ "U.S. Social Security Number (SSN)", @@ -479,7 +479,7 @@ error_rules contains SensitiveContent[2] if count(Rules.Credit_Card) == 0 # If error_rules contains any value, then some sensitive content # is not protected by any policy & check should fail. tests contains { - "PolicyId": "MS.DEFENDER.4.1v1", + "PolicyId": "MS.DEFENDER.4.1v2", "Criticality": "Shall", "Commandlet": ["Get-DlpComplianceRule"], "ActualValue": Rules, diff --git a/PowerShell/ScubaGear/Testing/Unit/PowerShell/CreateReport/CreateReportStubs/TestResults.json b/PowerShell/ScubaGear/Testing/Unit/PowerShell/CreateReport/CreateReportStubs/TestResults.json index 1ef3c17506..1e09521fce 100644 --- a/PowerShell/ScubaGear/Testing/Unit/PowerShell/CreateReport/CreateReportStubs/TestResults.json +++ b/PowerShell/ScubaGear/Testing/Unit/PowerShell/CreateReport/CreateReportStubs/TestResults.json @@ -1037,7 +1037,7 @@ "Get-MalwareFilterPolicy" ], "Criticality": "Should", - "PolicyId": "MS.DEFENDER.4.1v1", + "PolicyId": "MS.DEFENDER.4.1v2", "ReportDetails": "Requirement met", "RequirementMet": true }, @@ -1494,7 +1494,7 @@ "Locations": [ "All" ], - "Name": "MS.DEFENDER.4.1v1 Test", + "Name": "MS.DEFENDER.4.1v2 Test", "Workload": "Exchange, SharePoint, OneDriveForBusiness, Teams, EndpointDevices, OnPremisesScanner" } ], @@ -1527,7 +1527,7 @@ "Locations": [ "All" ], - "Name": "MS.DEFENDER.4.1v1 Test", + "Name": "MS.DEFENDER.4.1v2 Test", "Workload": "Exchange, SharePoint, OneDriveForBusiness, Teams, EndpointDevices, OnPremisesScanner" } ], @@ -1560,7 +1560,7 @@ "Locations": [ "All" ], - "Name": "MS.DEFENDER.4.1v1 Test", + "Name": "MS.DEFENDER.4.1v2 Test", "Workload": "Exchange, SharePoint, OneDriveForBusiness, Teams, EndpointDevices, OnPremisesScanner" } ], @@ -3574,4 +3574,4 @@ "ReportDetails": "1 meeting policy(ies) found that allow cloud recording and storage outside of the tenant\u0027s region: Tag:Custom Policy 1", "RequirementMet": false } -] \ No newline at end of file +] diff --git a/PowerShell/ScubaGear/Testing/Unit/Rego/Defender/DefenderConfig_04_test.rego b/PowerShell/ScubaGear/Testing/Unit/Rego/Defender/DefenderConfig_04_test.rego index 77a030a884..a0764b5094 100644 --- a/PowerShell/ScubaGear/Testing/Unit/Rego/Defender/DefenderConfig_04_test.rego +++ b/PowerShell/ScubaGear/Testing/Unit/Rego/Defender/DefenderConfig_04_test.rego @@ -9,7 +9,7 @@ import data.utils.report.NotCheckedDetails import rego.v1 # -# Policy MS.DEFENDER.4.1v1 +# Policy MS.DEFENDER.4.1v2 #-- test_ContentContainsSensitiveInformation_Correct_V1 if { Output := defender.tests with input.dlp_compliance_rules as [DlpComplianceRules] @@ -17,7 +17,7 @@ test_ContentContainsSensitiveInformation_Correct_V1 if { with input.defender_license as true with input.defender_dlp_license as true - TestResult("MS.DEFENDER.4.1v1", Output, PASS, true) == true + TestResult("MS.DEFENDER.4.1v2", Output, PASS, true) == true } test_AdvancedRule_Correct_V2 if { @@ -33,7 +33,7 @@ test_AdvancedRule_Correct_V2 if { with input.defender_license as true with input.defender_dlp_license as true - TestResult("MS.DEFENDER.4.1v1", Output, PASS, true) == true + TestResult("MS.DEFENDER.4.1v2", Output, PASS, true) == true } test_ContentContainsSensitiveInformation_Incorrect_V1 if { @@ -46,7 +46,7 @@ test_ContentContainsSensitiveInformation_Incorrect_V1 if { with input.defender_dlp_license as true ReportDetailString := "No matching rules found for: U.S. Social Security Number (SSN)" - TestResult("MS.DEFENDER.4.1v1", Output, ReportDetailString, false) == true + TestResult("MS.DEFENDER.4.1v2", Output, ReportDetailString, false) == true } test_ContentContainsSensitiveInformation_Incorrect_V2 if { @@ -59,7 +59,7 @@ test_ContentContainsSensitiveInformation_Incorrect_V2 if { with input.defender_dlp_license as true ReportDetailString := "No matching rules found for: U.S. Individual Taxpayer Identification Number (ITIN)" - TestResult("MS.DEFENDER.4.1v1", Output, ReportDetailString, false) == true + TestResult("MS.DEFENDER.4.1v2", Output, ReportDetailString, false) == true } test_ContentContainsSensitiveInformation_Incorrect_V3 if { @@ -72,7 +72,7 @@ test_ContentContainsSensitiveInformation_Incorrect_V3 if { with input.defender_dlp_license as true ReportDetailString := "No matching rules found for: Credit Card Number" - TestResult("MS.DEFENDER.4.1v1", Output, ReportDetailString, false) == true + TestResult("MS.DEFENDER.4.1v2", Output, ReportDetailString, false) == true } test_ContentContainsSensitiveInformation_Incorrect_V4 if { @@ -89,7 +89,7 @@ test_ContentContainsSensitiveInformation_Incorrect_V4 if { "U.S. Individual Taxpayer Identification Number (ITIN), U.S. Social Security Number (SSN)" ]) - TestResult("MS.DEFENDER.4.1v1", Output, ReportDetailString, false) == true + TestResult("MS.DEFENDER.4.1v2", Output, ReportDetailString, false) == true } test_ContentContainsSensitiveInformation_Incorrect_V5 if { @@ -106,7 +106,7 @@ test_ContentContainsSensitiveInformation_Incorrect_V5 if { "U.S. Individual Taxpayer Identification Number (ITIN), U.S. Social Security Number (SSN)" ]) - TestResult("MS.DEFENDER.4.1v1", Output, ReportDetailString, false) == true + TestResult("MS.DEFENDER.4.1v2", Output, ReportDetailString, false) == true } test_ContentContainsSensitiveInformation_Incorrect_V6 if { @@ -123,7 +123,7 @@ test_ContentContainsSensitiveInformation_Incorrect_V6 if { "U.S. Individual Taxpayer Identification Number (ITIN), U.S. Social Security Number (SSN)" ]) - TestResult("MS.DEFENDER.4.1v1", Output, ReportDetailString, false) == true + TestResult("MS.DEFENDER.4.1v2", Output, ReportDetailString, false) == true } test_NoDLPLicense_Incorrect_4_1_V1 if { @@ -131,7 +131,7 @@ test_NoDLPLicense_Incorrect_4_1_V1 if { with input.defender_dlp_license as false ReportDetailString := concat(" ", [FAIL, DLPLICENSEWARNSTR]) - TestResult("MS.DEFENDER.4.1v1", Output, ReportDetailString, false) == true + TestResult("MS.DEFENDER.4.1v2", Output, ReportDetailString, false) == true } #-- diff --git a/PowerShell/ScubaGear/baselines/defender.md b/PowerShell/ScubaGear/baselines/defender.md index bac372c45a..d866138218 100644 --- a/PowerShell/ScubaGear/baselines/defender.md +++ b/PowerShell/ScubaGear/baselines/defender.md @@ -401,15 +401,15 @@ confidence levels or adjust the levels in custom DLP policies to fit their environment and needs. ### Policies -#### MS.DEFENDER.4.1v1 -A custom policy SHALL be configured to protect PII and sensitive information, as defined by the agency. At a minimum, credit card numbers, U.S. Individual Taxpayer Identification Numbers (ITIN), and U.S. Social Security numbers (SSN) SHALL be blocked. +#### MS.DEFENDER.4.1v2 +A custom policy SHALL be configured to protect PII and sensitive information, as defined by the agency, blocking at a minimum: credit card numbers, U.S. Individual Taxpayer Identification Numbers (ITIN), and U.S. Social Security numbers (SSN). - + - _Rationale:_ Users may inadvertently share sensitive information with others who should not have access to it. DLP policies provide a way for agencies to detect and prevent unauthorized disclosures. -- _Last modified:_ June 2023 +- _Last modified:_ November 2024 - _MITRE ATT&CK TTP Mapping:_ - [T1567: Exfiltration Over Web Service](https://attack.mitre.org/techniques/T1567/) - [T1530: Data from Cloud Storage](https://attack.mitre.org/techniques/T1530/) @@ -424,7 +424,7 @@ The custom policy SHOULD be applied to Exchange, OneDrive, SharePoint, Teams cha affected locations to be effective. - _Last modified:_ June 2023 - _Note:_ The custom policy referenced here is the same policy - configured in [MS.DEFENDER.4.1v1](#msdefender41v1). + configured in [MS.DEFENDER.4.1v2](#msdefender41v2). - _MITRE ATT&CK TTP Mapping:_ - [T1567: Exfiltration Over Web Service](https://attack.mitre.org/techniques/T1567/) - [T1530: Data from Cloud Storage](https://attack.mitre.org/techniques/T1530/) @@ -440,7 +440,7 @@ The action for the custom policy SHOULD be set to block sharing sensitive inform on agency policies and valid business justifications. - _Last modified:_ June 2023 - _Note:_ The custom policy referenced here is the same policy - configured in [MS.DEFENDER.4.1v1](#msdefender41v1). + configured in [MS.DEFENDER.4.1v2](#msdefender41v2). - _MITRE ATT&CK TTP Mapping:_ - [T1567: Exfiltration Over Web Service](https://attack.mitre.org/techniques/T1567/) - [T1530: Data from Cloud Storage](https://attack.mitre.org/techniques/T1530/) @@ -456,7 +456,7 @@ Notifications to inform users and help educate them on the proper use of sensiti accessing sensitive information. - _Last modified:_ June 2023 - _Note:_ The custom policy referenced here is the same policy - configured in [MS.DEFENDER.4.1v1](#msdefender41v1). + configured in [MS.DEFENDER.4.1v2](#msdefender41v2). - _MITRE ATT&CK TTP Mapping:_ - None @@ -489,7 +489,7 @@ information by restricted apps and unwanted Bluetooth applications. - _Last modified:_ June 2023 - _Note:_ - The custom policy referenced here is the same policy - configured in [MS.DEFENDER.4.1v1](#msdefender41v1). + configured in [MS.DEFENDER.4.1v2](#msdefender41v2). - This action can only be included if at least one device is onboarded to the agency tenant. Otherwise, the option to block restricted apps will not be available. @@ -532,7 +532,7 @@ information by restricted apps and unwanted Bluetooth applications. ### Implementation -#### MS.DEFENDER.4.1v1 Instructions +#### MS.DEFENDER.4.1v2 Instructions 1. Sign in to the **Microsoft Purview compliance portal**. @@ -595,18 +595,18 @@ information by restricted apps and unwanted Bluetooth applications. #### MS.DEFENDER.4.2v1 Instructions -See [MS.DEFENDER.4.1v1 Instructions](#msdefender41v1-instructions) step 8 +See [MS.DEFENDER.4.1v2 Instructions](#msdefender41v2-instructions) step 8 for details on enforcing DLP policy in specific M365 service locations. #### MS.DEFENDER.4.3v1 Instructions -See [MS.DEFENDER.4.1v1 Instructions](#msdefender41v1-instructions) steps +See [MS.DEFENDER.4.1v2 Instructions](#msdefender41v2-instructions) steps 15-17 for details on configuring DLP policy to block sharing sensitive information with everyone. #### MS.DEFENDER.4.4v1 Instructions -See [MS.DEFENDER.4.1v1 Instructions](#msdefender41v1-instructions) steps +See [MS.DEFENDER.4.1v2 Instructions](#msdefender41v2-instructions) steps 18-19 for details on configuring DLP policy to notify users when accessing sensitive information. @@ -645,7 +645,7 @@ before the instructions below can be completed. 3. Select **Policies** from the top of the page. 4. Find the custom DLP policy configured under - [MS.DEFENDER.4.1v1 Instructions](#msdefender41v1-instructions) in the list + [MS.DEFENDER.4.1v2 Instructions](#msdefender41v2-instructions) in the list and click the Policy name to select. 5. Select **Edit Policy**. diff --git a/PowerShell/ScubaGear/baselines/exo.md b/PowerShell/ScubaGear/baselines/exo.md index 7d515db945..2d23290f3e 100644 --- a/PowerShell/ScubaGear/baselines/exo.md +++ b/PowerShell/ScubaGear/baselines/exo.md @@ -621,14 +621,14 @@ At a minimum, the DLP solution SHALL restrict sharing credit card numbers, U.S. Any product meeting the requirements outlined in this baseline policy may be used. If the agency uses Microsoft Defender, see the following implementation steps for [DLP](./defender.md#implementation-3) for additional guidance. #### MS.EXO.8.2v2 Instructions -Any product meeting the requirements outlined in this baseline policy may be used. If the agency uses Microsoft Defender, see the following implementation steps for [protecting PII](./defender.md#msdefender41v1-instructions) for additional guidance. +Any product meeting the requirements outlined in this baseline policy may be used. If the agency uses Microsoft Defender, see the following implementation steps for [protecting PII](./defender.md#msdefender41v2-instructions) for additional guidance. #### MS.EXO.8.3v1 Instructions Any product meeting the requirements outlined in this baseline policy may be used. If the agency uses Microsoft Defender, see the following implementation steps for [DLP](./defender.md#implementation-3) for additional guidance. #### MS.EXO.8.4v1 Instructions -Any product meeting the requirements outlined in this baseline policy may be used. If the agency uses Microsoft Defender, see the following implementation steps for [protecting PII](./defender.md#msdefender41v1-instructions) for additional guidance. +Any product meeting the requirements outlined in this baseline policy may be used. If the agency uses Microsoft Defender, see the following implementation steps for [protecting PII](./defender.md#msdefender41v2-instructions) for additional guidance. ## 9. Attachment File Type @@ -1075,7 +1075,7 @@ Mailbox auditing SHALL be enabled. - [T1586.002: Email Accounts](https://attack.mitre.org/techniques/T1586/002/) - [T1564: Hide Artifacts](https://attack.mitre.org/techniques/T1564/) - [T1564.008: Email Hiding Rules](https://attack.mitre.org/techniques/T1564/008/) - + ### Resources - [Manage mailbox auditing in Office 365 \| Microsoft diff --git a/Testing/Functional/Products/TestPlans/defender.testplan.yaml b/Testing/Functional/Products/TestPlans/defender.testplan.yaml index efbbd1eb05..8eb81fb905 100644 --- a/Testing/Functional/Products/TestPlans/defender.testplan.yaml +++ b/Testing/Functional/Products/TestPlans/defender.testplan.yaml @@ -150,10 +150,10 @@ TestPlan: Postconditions: [] ExpectedResult: true - - PolicyId: MS.DEFENDER.4.1v1 + - PolicyId: MS.DEFENDER.4.1v2 TestDriver: ScubaCached Tests: - - TestDescription: MS.DEFENDER.4.1v1 Compliant case - Advanced Rule + - TestDescription: MS.DEFENDER.4.1v2 Compliant case - Advanced Rule Preconditions: - Command: UpdateProviderExport Splat: @@ -176,7 +176,7 @@ TestPlan: NotifyUserType: "NotSet" Postconditions: [] ExpectedResult: true - - TestDescription: MS.DEFENDER.4.1v1 Compliant case - Not Advanced Rule + - TestDescription: MS.DEFENDER.4.1v2 Compliant case - Not Advanced Rule Preconditions: - Command: UpdateProviderExport Splat: @@ -205,7 +205,7 @@ TestPlan: NotifyUserType: "NotSet" Postconditions: [] ExpectedResult: true - - TestDescription: MS.DEFENDER.4.1v1 Non-Compliant case - Advanced Rule, Disabled + - TestDescription: MS.DEFENDER.4.1v2 Non-Compliant case - Advanced Rule, Disabled Preconditions: - Command: UpdateProviderExport Splat: @@ -224,7 +224,7 @@ TestPlan: Disabled: true Postconditions: [] ExpectedResult: false - - TestDescription: MS.DEFENDER.4.1v1 Non-Compliant case - Not Advanced Rule, Disabled + - TestDescription: MS.DEFENDER.4.1v2 Non-Compliant case - Not Advanced Rule, Disabled Preconditions: - Command: UpdateProviderExport Splat: @@ -249,7 +249,7 @@ TestPlan: Disabled: true Postconditions: [] ExpectedResult: false - - TestDescription: MS.DEFENDER.4.1v1 Non-Compliant case - Advanced Rule, Enabled + - TestDescription: MS.DEFENDER.4.1v2 Non-Compliant case - Advanced Rule, Enabled Preconditions: - Command: UpdateProviderExport Splat: @@ -272,7 +272,7 @@ TestPlan: NotifyUserType: "NotSet" Postconditions: [] ExpectedResult: false - - TestDescription: MS.DEFENDER.4.1v1 Non-Compliant case - Not Advanced Rule, Enabled + - TestDescription: MS.DEFENDER.4.1v2 Non-Compliant case - Not Advanced Rule, Enabled Preconditions: - Command: UpdateProviderExport Splat: @@ -301,7 +301,7 @@ TestPlan: NotifyUserType: "NotSet" Postconditions: [] ExpectedResult: false - - TestDescription: MS.DEFENDER.4.1v1 Non-Compliant case - Advanced Rule, Mode + - TestDescription: MS.DEFENDER.4.1v2 Non-Compliant case - Advanced Rule, Mode Preconditions: - Command: UpdateProviderExport Splat: @@ -324,7 +324,7 @@ TestPlan: NotifyUserType: "NotSet" Postconditions: [] ExpectedResult: false - - TestDescription: MS.DEFENDER.4.1v1 Non-Compliant case - Not Advanced Rule, Mode + - TestDescription: MS.DEFENDER.4.1v2 Non-Compliant case - Not Advanced Rule, Mode Preconditions: - Command: UpdateProviderExport Splat: @@ -353,7 +353,7 @@ TestPlan: NotifyUserType: "NotSet" Postconditions: [] ExpectedResult: false - - TestDescription: MS.DEFENDER.4.1v1 Non-Compliant case - Advanced Rule, Missing U.S. Social Security Number (SSN) + - TestDescription: MS.DEFENDER.4.1v2 Non-Compliant case - Advanced Rule, Missing U.S. Social Security Number (SSN) Preconditions: - Command: UpdateProviderExport Splat: @@ -376,7 +376,7 @@ TestPlan: NotifyUserType: "NotSet" Postconditions: [] ExpectedResult: false - - TestDescription: MS.DEFENDER.4.1v1 Non-Compliant case - Not Advanced Rule, Missing U.S. Social Security Number (SSN) + - TestDescription: MS.DEFENDER.4.1v2 Non-Compliant case - Not Advanced Rule, Missing U.S. Social Security Number (SSN) Preconditions: - Command: UpdateProviderExport Splat: @@ -403,7 +403,7 @@ TestPlan: NotifyUserType: "NotSet" Postconditions: [] ExpectedResult: false - - TestDescription: MS.DEFENDER.4.1v1 Non-Compliant case - Advanced Rule, Missing U.S. Individual Taxpayer Identification Number (ITIN) + - TestDescription: MS.DEFENDER.4.1v2 Non-Compliant case - Advanced Rule, Missing U.S. Individual Taxpayer Identification Number (ITIN) Preconditions: - Command: UpdateProviderExport Splat: @@ -426,7 +426,7 @@ TestPlan: NotifyUserType: "NotSet" Postconditions: [] ExpectedResult: false - - TestDescription: MS.DEFENDER.4.1v1 Non-Compliant case - Not Advanced Rule, Missing U.S. Individual Taxpayer Identification Number (ITIN) + - TestDescription: MS.DEFENDER.4.1v2 Non-Compliant case - Not Advanced Rule, Missing U.S. Individual Taxpayer Identification Number (ITIN) Preconditions: - Command: UpdateProviderExport Splat: @@ -453,7 +453,7 @@ TestPlan: NotifyUserType: "NotSet" Postconditions: [] ExpectedResult: false - - TestDescription: MS.DEFENDER.4.1v1 Non-Compliant case - Advanced Rule, Missing Credit Card + - TestDescription: MS.DEFENDER.4.1v2 Non-Compliant case - Advanced Rule, Missing Credit Card Preconditions: - Command: UpdateProviderExport Splat: @@ -476,7 +476,7 @@ TestPlan: NotifyUserType: "NotSet" Postconditions: [] ExpectedResult: false - - TestDescription: MS.DEFENDER.4.1v1 Non-Compliant case - Not Advanced Rule, Missing Credit Card + - TestDescription: MS.DEFENDER.4.1v2 Non-Compliant case - Not Advanced Rule, Missing Credit Card Preconditions: - Command: UpdateProviderExport Splat: @@ -503,7 +503,7 @@ TestPlan: NotifyUserType: "NotSet" Postconditions: [] ExpectedResult: false - - TestDescription: MS.DEFENDER.4.1v1 Non-Compliant case - Advanced Rule, ParentPolicyName + - TestDescription: MS.DEFENDER.4.1v2 Non-Compliant case - Advanced Rule, ParentPolicyName Preconditions: - Command: UpdateProviderExport Splat: @@ -526,7 +526,7 @@ TestPlan: NotifyUserType: "NotSet" Postconditions: [] ExpectedResult: false - - TestDescription: MS.DEFENDER.4.1v1 Non-Compliant case - Not Advanced Rule, ParentPolicyName + - TestDescription: MS.DEFENDER.4.1v2 Non-Compliant case - Not Advanced Rule, ParentPolicyName Preconditions: - Command: UpdateProviderExport Splat: