From 960490bfa5e330791c9aada25a4b2f9ace82f178 Mon Sep 17 00:00:00 2001 From: Alden Hilton Date: Tue, 12 Nov 2024 16:56:27 -0800 Subject: [PATCH 01/40] Initial implementation of adding UUID to the file name --- PowerShell/ScubaGear/Modules/Orchestrator.psm1 | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index f762231122..ebbcbe9228 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -888,10 +888,13 @@ function ConvertTo-ResultsCsv { ) process { try { - $ScubaResultsFileName = Join-Path $OutFolderPath -ChildPath "$OutJsonFileName.json" + # Wildcard * in next line is to match the UUID in the file name + $ScubaResultsFileName = Join-Path $OutFolderPath -ChildPath "$OutJsonFileName*.json" if (Test-Path $ScubaResultsFileName -PathType Leaf) { # The ScubaResults file exists, no need to look for the individual json files - $ScubaResults = Get-Content $ScubaResultsFileName | ConvertFrom-Json + # As there is the possibility that the wildcard will match multiple files, + # select the one that was created last if there are multiple. + $ScubaResults = Get-Content (Get-ChildItem $ScubaResultsFileName | Sort-Object CreationTime -Descending | Select-Object -First 1).FullName } else { # The ScubaResults file does not exists, so we need to look inside the IndividualReports @@ -1071,7 +1074,8 @@ function Merge-JsonOutput { $ReportJson = $ReportJson.replace("\u0027", "'") # Save the file - $JsonFileName = Join-Path -Path $OutFolderPath "$($OutJsonFileName).json" -ErrorAction 'Stop' + $JsonFileName = Join-Path -Path $OutFolderPath "$($OutJsonFileName)_$($ReportUuid).json" ` + -ErrorAction 'Stop' $ReportJson | Set-Content -Path $JsonFileName -Encoding $(Get-FileEncoding) -ErrorAction 'Stop' # Delete the now redundant files @@ -1802,8 +1806,10 @@ 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. + $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 From e9f353b1d422d71d44b37b2d67358935a16e87a5 Mon Sep 17 00:00:00 2001 From: Alden Hilton Date: Wed, 13 Nov 2024 09:21:26 -0800 Subject: [PATCH 02/40] Add back missing ConvertFrom-Json call --- PowerShell/ScubaGear/Modules/Orchestrator.psm1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index ebbcbe9228..93b436109e 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -894,7 +894,7 @@ function ConvertTo-ResultsCsv { # The ScubaResults file exists, no need to look for the individual json files # As there is the possibility that the wildcard will match multiple files, # select the one that was created last if there are multiple. - $ScubaResults = Get-Content (Get-ChildItem $ScubaResultsFileName | Sort-Object CreationTime -Descending | Select-Object -First 1).FullName + $ScubaResults = Get-Content (Get-ChildItem $ScubaResultsFileName | Sort-Object CreationTime -Descending | Select-Object -First 1).FullName | ConvertFrom-Json } else { # The ScubaResults file does not exists, so we need to look inside the IndividualReports From d7e44a6e0c9c9d6863826a9aa79274950ecaa6ee Mon Sep 17 00:00:00 2001 From: Alden Hilton Date: Wed, 13 Nov 2024 10:26:24 -0800 Subject: [PATCH 03/40] Mock Get-ChildItem in unit tests --- .../Unit/PowerShell/Orchestrator/ConvertTo-ResultsCsv.Tests.ps1 | 1 + .../Unit/PowerShell/Orchestrator/Invoke-ScubaCached.Tests.ps1 | 1 + 2 files changed, 2 insertions(+) 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..474e6ac0e5 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,7 @@ InModuleScope Orchestrator { Mock -CommandName Get-FileEncoding Mock -CommandName ConvertTo-Csv { "" } Mock -CommandName Write-Warning {} + Mock -CommandName Get-ChildItem { @{"FullName"="ScubaResults.json"; "CreationTime"=$(Get-Date)} } } It 'Handles multiple products, control groups, and controls' { 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..90de8754b9 100644 --- a/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Invoke-ScubaCached.Tests.ps1 +++ b/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Invoke-ScubaCached.Tests.ps1 @@ -30,6 +30,7 @@ InModuleScope Orchestrator { Mock -CommandName Get-Content {} Mock -CommandName Get-Member { $true } Mock -CommandName New-Guid { "00000000-0000-0000-0000-000000000000" } + Mock -CommandName Get-ChildItem { @{"FullName"="ScubaResults.json"; "CreationTime"=$(Get-Date)} } } Context 'When checking the conformance of commercial tenants' { BeforeAll { From f91b119a4d0cf137fe13c461df424d054211b73f Mon Sep 17 00:00:00 2001 From: Alden Hilton Date: Wed, 13 Nov 2024 12:29:10 -0800 Subject: [PATCH 04/40] Document addition of UUID to ScubaResults file name --- PowerShell/ScubaGear/Modules/Orchestrator.psm1 | 4 ++-- ...=> ScubaResults_21189b0e-f045-43ee-b9ba-653b32744e45.json} | 4 +++- docs/configuration/parameters.md | 3 ++- docs/execution/reports.md | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) rename PowerShell/ScubaGear/Sample-Reports/{ScubaResults.json => ScubaResults_21189b0e-f045-43ee-b9ba-653b32744e45.json} (99%) diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index 93b436109e..08caff36a7 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". @@ -1602,7 +1602,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". 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/docs/configuration/parameters.md b/docs/configuration/parameters.md index ed9fdbf1b0..4934056e04 100644 --- a/docs/configuration/parameters.md +++ b/docs/configuration/parameters.md @@ -274,7 +274,7 @@ 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 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 | |-------------|----------------| @@ -290,6 +290,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 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 c1e0b1c7485b42554f2d40b00ebcdba1ffd693fc Mon Sep 17 00:00:00 2001 From: Alden Hilton Date: Wed, 13 Nov 2024 16:00:09 -0800 Subject: [PATCH 05/40] Add unit tests for when there are multiple ScubaResults*.json files --- .../ConvertTo-ResultsCsv.Tests.ps1 | 32 ++++++++++++++++++- .../Orchestrator/Invoke-ScubaCached.Tests.ps1 | 25 ++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) 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 474e6ac0e5..6d8eef4ff6 100644 --- a/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/ConvertTo-ResultsCsv.Tests.ps1 +++ b/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/ConvertTo-ResultsCsv.Tests.ps1 @@ -15,7 +15,9 @@ InModuleScope Orchestrator { Mock -CommandName Get-FileEncoding Mock -CommandName ConvertTo-Csv { "" } Mock -CommandName Write-Warning {} - Mock -CommandName Get-ChildItem { @{"FullName"="ScubaResults.json"; "CreationTime"=$(Get-Date)} } + Mock -CommandName Get-ChildItem { + [pscustomobject]@{"FullName"="ScubaResults.json"; "CreationTime"=[DateTime]"2024-01-01"} + } } It 'Handles multiple products, control groups, and controls' { @@ -85,6 +87,34 @@ InModuleScope Orchestrator { Should -Invoke -CommandName Format-PlainText -Exactly -Times 0 Should -Invoke -CommandName Write-Warning -Exactly -Times 1 } + + It 'Handles multiple ScubaResults*.json file names' { + # If this function is called from Invoke-ScubaCached, 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. + 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 + } + } + + $CsvParameters = @{ + ProductNames = @("exo"); + OutFolderPath = "."; + OutJsonFileName = "ScubaResults"; + OutCsvFileName = "ScubaResults"; + OutActionPlanFileName = "ActionPlan"; + } + { ConvertTo-ResultsCsv @CsvParameters} | Should -Not -Throw + Should -Invoke -CommandName Write-Warning -Exactly -Times 0 + } } } 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 90de8754b9..0cbad16a3f 100644 --- a/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Invoke-ScubaCached.Tests.ps1 +++ b/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Invoke-ScubaCached.Tests.ps1 @@ -30,7 +30,9 @@ InModuleScope Orchestrator { Mock -CommandName Get-Content {} Mock -CommandName Get-Member { $true } Mock -CommandName New-Guid { "00000000-0000-0000-0000-000000000000" } - Mock -CommandName Get-ChildItem { @{"FullName"="ScubaResults.json"; "CreationTime"=$(Get-Date)} } + Mock -CommandName Get-ChildItem { + [pscustomobject]@{"FullName"="ScubaResults.json"; "CreationTime"=[DateTime]"2024-01-01"} + } } Context 'When checking the conformance of commercial tenants' { BeforeAll { @@ -167,6 +169,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 + } + } } } From 6451c12cac07ad844822bcb31ea0a046d337b595 Mon Sep 17 00:00:00 2001 From: Alden Hilton <106177711+adhilto@users.noreply.github.com> Date: Wed, 13 Nov 2024 16:04:32 -0800 Subject: [PATCH 06/40] Correct minor typo in documentation --- docs/configuration/parameters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/parameters.md b/docs/configuration/parameters.md index 4934056e04..4fdeec1321 100644 --- a/docs/configuration/parameters.md +++ b/docs/configuration/parameters.md @@ -274,7 +274,7 @@ Invoke-SCuBA -ProductNames teams ` ## OutJsonFileName -**OutJsonFileName** specifies the base file name 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. +**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 | |-------------|----------------| From e63356e00cdcfacbb56c6c837fa742d93904dc8a Mon Sep 17 00:00:00 2001 From: buidav <105074908+buidav@users.noreply.github.com> Date: Fri, 15 Nov 2024 16:11:22 -0800 Subject: [PATCH 07/40] remove wildcard search in ConvertTo-ResultsCSV code path --- .../ScubaGear/Modules/Orchestrator.psm1 | 67 ++++++++++++------- 1 file changed, 41 insertions(+), 26 deletions(-) diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index 08caff36a7..e1e40bb4bf 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -374,22 +374,34 @@ function Invoke-SCuBA { # Tenant Metadata for the Report $TenantDetails = Get-TenantDetail -ProductNames $ScubaConfig.ProductNames -M365Environment $ScubaConfig.M365Environment + # Generate the GUID for the JSON + # TODO Stick the GUID within the ScubaConfig object to clean up parameter bloat + 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 + } + 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 @@ -438,10 +450,11 @@ function Invoke-SCuBA { } # Craft the csv version of just the results $CsvParams = @{ - 'ProductNames' = $ScubaConfig.ProductNames; - 'OutFolderPath' = $OutFolderPath; - 'OutJsonFileName' = $ScubaConfig.OutJsonFileName; - 'OutCsvFileName' = $ScubaConfig.OutCsvFileName; + 'ProductNames' = $ScubaConfig.ProductNames; + 'Guid' = $Guid; + 'OutFolderPath' = $OutFolderPath; + 'OutJsonFileName' = $ScubaConfig.OutJsonFileName; + 'OutCsvFileName' = $ScubaConfig.OutCsvFileName; 'OutActionPlanFileName' = $ScubaConfig.OutActionPlanFileName; } ConvertTo-ResultsCsv @CsvParams @@ -540,6 +553,11 @@ function Invoke-ProviderList { [string] $OutProviderFileName, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $Guid, + [Parameter(Mandatory = $true)] [hashtable] $BoundParameters @@ -632,15 +650,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", @@ -866,6 +875,11 @@ function ConvertTo-ResultsCsv { [string[]] $ProductNames, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $Guid, + [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] @@ -888,13 +902,11 @@ function ConvertTo-ResultsCsv { ) process { try { - # Wildcard * in next line is to match the UUID in the file name - $ScubaResultsFileName = Join-Path $OutFolderPath -ChildPath "$OutJsonFileName*.json" + # Fine the ScubaResults file with UUID in the file name. + $ScubaResultsFileName = Join-Path $OutFolderPath -ChildPath "$($OutJsonFileName)_$($Guid).json" if (Test-Path $ScubaResultsFileName -PathType Leaf) { # The ScubaResults file exists, no need to look for the individual json files - # As there is the possibility that the wildcard will match multiple files, - # select the one that was created last if there are multiple. - $ScubaResults = Get-Content (Get-ChildItem $ScubaResultsFileName | Sort-Object CreationTime -Descending | Select-Object -First 1).FullName | ConvertFrom-Json + $ScubaResults = Get-Content (Get-ChildItem $ScubaResultsFileName).FullName | ConvertFrom-Json } else { # The ScubaResults file does not exists, so we need to look inside the IndividualReports @@ -1809,6 +1821,9 @@ function Invoke-SCuBACached { $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 From 80fc0a43c74a8f6d5babe8bfe132b91f9751796e Mon Sep 17 00:00:00 2001 From: buidav <105074908+buidav@users.noreply.github.com> Date: Fri, 15 Nov 2024 17:01:52 -0800 Subject: [PATCH 08/40] add error handling of window path length limit errors --- .../ScubaGear/Modules/Orchestrator.psm1 | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index e1e40bb4bf..9cb54b4d55 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -1085,6 +1085,29 @@ function Merge-JsonOutput { $ReportJson = $ReportJson.replace("\u003e", ">") $ReportJson = $ReportJson.replace("\u0027", "'") + + + # Check if the absolute final results output path is greater than the allowable windows file Path length + # Trim the GUID to save some character length space if it is. + $MAX_WINDOWS_PATH_LEN = 256 + $CharactersToTrim = 13 + $CurrentLocation = (Get-Location) | Select-Object -ExpandProperty ProviderPath + $JoinedFilePath = Join-Path -Path $CurrentLocation -ChildPath (Join-Path -Path $OutFolderPath -ChildPath "$($OutJsonFileName)_$($ReportUuid)") + $AbsoluteResultsFilePathLen = ([System.IO.Path]::GetFullPath($JoinedFilePath)).Length + + if ($AbsoluteResultsFilePathLen -gt ($MAX_WINDOWS_PATH_LEN + $CharactersToTrim)) { + $PathLengthErrorMessage = "ScubaGear was executed in a location where the maximum file path length is greater than the allowable Windows File System limit ` + Please execute ScubaGear in a location where for Window File Path limit is less than $($MAX_WINDOWS_PATH_LEN). The current length is $($AbsoluteResultsFilePathLen)" + throw $PathLengthErrorMessage + } + elseif ($AbsoluteResultsFilePathLen -gt $MAX_WINDOWS_PATH_LEN) { + $ReportUuid = $ReportUuid.Substring(0, $ReportUuid.Length - $CharactersToTrim) + $PathLengthErrorMessage = "The GUID appended to the ScubaResults file name was truncated by $CharactersToTrim ` + because ScubaGear was executed in a location where the Windows Absolute file path was longer than $($MAX_WINDOWS_PATH_LEN) + This should not have affected the content of the final output but please proceed with caution when moving the file to other directories" + Write-Warning $PathLengthErrorMessage + } + # Save the file $JsonFileName = Join-Path -Path $OutFolderPath "$($OutJsonFileName)_$($ReportUuid).json" ` -ErrorAction 'Stop' From 3c504a3f16e7149d244bb3577b166f572cf4a74a Mon Sep 17 00:00:00 2001 From: buidav <105074908+buidav@users.noreply.github.com> Date: Fri, 15 Nov 2024 19:07:01 -0800 Subject: [PATCH 09/40] fix some of the tests --- .../ScubaGear/Modules/Orchestrator.psm1 | 6 ++-- .../ConvertTo-ResultsCsv.Tests.ps1 | 29 ++++++++++--------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index 9cb54b4d55..5861bd71c0 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -902,7 +902,7 @@ function ConvertTo-ResultsCsv { ) process { try { - # Fine the ScubaResults file with UUID in the file name. + # Find the ScubaResults file with UUID in the file name. $ScubaResultsFileName = Join-Path $OutFolderPath -ChildPath "$($OutJsonFileName)_$($Guid).json" if (Test-Path $ScubaResultsFileName -PathType Leaf) { # The ScubaResults file exists, no need to look for the individual json files @@ -1830,6 +1830,7 @@ function Invoke-SCuBACached { 'ModuleVersion' = $ModuleVersion; 'OutFolderPath' = $OutFolderPath; 'OutProviderFileName' = $OutProviderFileName; + 'Guid' = $Guid; 'BoundParameters' = $PSBoundParameters; } Invoke-ProviderList @ProviderParams @@ -1863,7 +1864,7 @@ function Invoke-SCuBACached { $Guid = New-Guid -ErrorAction 'Stop' } catch { - $Guid = "00000000-0000-0000-0000-000000000000" + $Guid = "00000000-0000-0000]-0000-000000000000" $Warning = "Error generating new UUID. See the exception message for more details: $($_)" Write-Warning $Warning } @@ -1912,6 +1913,7 @@ function Invoke-SCuBACached { # Craft the csv version of just the results $CsvParams = @{ 'ProductNames' = $ProductNames; + 'Guid' = $Guid; 'OutFolderPath' = $OutFolderPath; 'OutJsonFileName' = $OutJsonFileName; 'OutCsvFileName' = $OutCsvFileName; 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 6d8eef4ff6..a22f9090f3 100644 --- a/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/ConvertTo-ResultsCsv.Tests.ps1 +++ b/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/ConvertTo-ResultsCsv.Tests.ps1 @@ -16,7 +16,7 @@ InModuleScope Orchestrator { Mock -CommandName ConvertTo-Csv { "" } Mock -CommandName Write-Warning {} Mock -CommandName Get-ChildItem { - [pscustomobject]@{"FullName"="ScubaResults.json"; "CreationTime"=[DateTime]"2024-01-01"} + [pscustomobject]@{"FullName"="ScubaResults_00000000-0000-0000-0000-000000000000.json"; "CreationTime"=[DateTime]"2024-01-01"} } } @@ -59,11 +59,12 @@ InModuleScope Orchestrator { }} } $CsvParameters = @{ - ProductNames = @("exo", "aad"); - OutFolderPath = "."; - OutJsonFileName = "ScubaResults"; - OutCsvFileName = "ScubaResults"; + ProductNames = @("exo", "aad"); + OutFolderPath = "."; + OutJsonFileName = "ScubaResults"; + OutCsvFileName = "ScubaResults"; OutActionPlanFileName = "ActionPlan"; + Guid = "00000000-0000-0000-0000-000000000000"; } { ConvertTo-ResultsCsv @CsvParameters} | Should -Not -Throw Should -Invoke -CommandName ConvertFrom-Json -Exactly -Times 1 @@ -77,11 +78,12 @@ 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 = "."; + OutJsonFileName = "ScubaResults"; + OutCsvFileName = "ScubaResults"; OutActionPlanFileName = "ActionPlan"; + Guid = "00000000-0000-0000-0000-000000000000"; } { ConvertTo-ResultsCsv @CsvParameters} | Should -Not -Throw Should -Invoke -CommandName Format-PlainText -Exactly -Times 0 @@ -93,14 +95,12 @@ InModuleScope Orchestrator { # there are multiple files matching "ScubaResults*.json". In this case, ScubaGear should # choose the file created most recently. 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"} + [pscustomobject]@{"FullName"="ScubaResults_00000000-0000-0000-0000-000000000000.json"; "CreationTime"=[DateTime]"2024-01-01"} ) } Mock -CommandName Get-Content { - if ($Path -ne "ScubaResultsNew.json") { - # Should be the new one, throw if not + if ($Path -ne "ScubaResults_00000000-0000-0000-0000-000000000000.json") { + # Should be the exact file name, throw if not throw } } @@ -111,6 +111,7 @@ InModuleScope Orchestrator { OutJsonFileName = "ScubaResults"; OutCsvFileName = "ScubaResults"; OutActionPlanFileName = "ActionPlan"; + Guid = "00000000-0000-0000-0000-000000000000"; } { ConvertTo-ResultsCsv @CsvParameters} | Should -Not -Throw Should -Invoke -CommandName Write-Warning -Exactly -Times 0 From 8dcb093931f8372a36aa687141dd812a613ed5f7 Mon Sep 17 00:00:00 2001 From: buidav <105074908+buidav@users.noreply.github.com> Date: Fri, 15 Nov 2024 19:11:15 -0800 Subject: [PATCH 10/40] fix the all the current broken unit tests --- PowerShell/ScubaGear/Modules/Orchestrator.psm1 | 2 +- .../Orchestrator/Invoke-ProviderList.Tests.ps1 | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index 5861bd71c0..39175ac46f 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -1864,7 +1864,7 @@ function Invoke-SCuBACached { $Guid = New-Guid -ErrorAction 'Stop' } catch { - $Guid = "00000000-0000-0000]-0000-000000000000" + $Guid = "00000000-0000-0000-0000-000000000000" $Warning = "Error generating new UUID. See the exception message for more details: $($_)" Write-Warning $Warning } 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' { From f351444fed5b0e7c9301a3dc39f130a24d269fdf Mon Sep 17 00:00:00 2001 From: buidav <105074908+buidav@users.noreply.github.com> Date: Sun, 17 Nov 2024 19:19:05 -0800 Subject: [PATCH 11/40] additional unit tests --- .../ScubaGear/Modules/Orchestrator.psm1 | 6 ++- .../Sample-Config-Files/full_config.yaml | 4 +- .../Orchestrator/Merge-JsonOutput.Tests.ps1 | 43 +++++++++++++------ 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index 39175ac46f..8ac109cd10 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -1095,15 +1095,17 @@ function Merge-JsonOutput { $JoinedFilePath = Join-Path -Path $CurrentLocation -ChildPath (Join-Path -Path $OutFolderPath -ChildPath "$($OutJsonFileName)_$($ReportUuid)") $AbsoluteResultsFilePathLen = ([System.IO.Path]::GetFullPath($JoinedFilePath)).Length + # Throw an error if the path length is too long. if ($AbsoluteResultsFilePathLen -gt ($MAX_WINDOWS_PATH_LEN + $CharactersToTrim)) { $PathLengthErrorMessage = "ScubaGear was executed in a location where the maximum file path length is greater than the allowable Windows File System limit ` - Please execute ScubaGear in a location where for Window File Path limit is less than $($MAX_WINDOWS_PATH_LEN). The current length is $($AbsoluteResultsFilePathLen)" + Please execute ScubaGear in a directory where for Window file path limit is less than $($MAX_WINDOWS_PATH_LEN).` + Your current file path length is $($AbsoluteResultsFilePathLen)" throw $PathLengthErrorMessage } elseif ($AbsoluteResultsFilePathLen -gt $MAX_WINDOWS_PATH_LEN) { $ReportUuid = $ReportUuid.Substring(0, $ReportUuid.Length - $CharactersToTrim) $PathLengthErrorMessage = "The GUID appended to the ScubaResults file name was truncated by $CharactersToTrim ` - because ScubaGear was executed in a location where the Windows Absolute file path was longer than $($MAX_WINDOWS_PATH_LEN) + because ScubaGear was executed in a location where the Windows absolute file path length was longer than $($MAX_WINDOWS_PATH_LEN) This should not have affected the content of the final output but please proceed with caution when moving the file to other directories" Write-Warning $PathLengthErrorMessage } diff --git a/PowerShell/ScubaGear/Sample-Config-Files/full_config.yaml b/PowerShell/ScubaGear/Sample-Config-Files/full_config.yaml index 0094aa2463..23f6e2eb08 100644 --- a/PowerShell/ScubaGear/Sample-Config-Files/full_config.yaml +++ b/PowerShell/ScubaGear/Sample-Config-Files/full_config.yaml @@ -19,9 +19,7 @@ OutFolderName: M365BaselineConformance OutProviderFileName: ProviderSettingsExport OutRegoFileName: TestResults OutReportName: BaselineReports -Organization: abcdef.example.com -AppID: 0123456789abcdef01234566789abcde -CertificateThumbprint: fedcba9876543210fedcba9876543210fedcba98 + OutJsonFileName: ScubaResults OutCsvFileName: ScubaResults OutActionPlanFileName: ActionPlan 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..c272dbd5ff 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,15 @@ 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" = ""; + } } Mock -CommandName Add-Member {} Mock -CommandName ConvertTo-Json { "" } @@ -22,37 +21,55 @@ InModuleScope Orchestrator { BeforeAll { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'JsonParameters')] $JsonParameters = @{ - TenantDetails = @{"DisplayName" = "displayName"; "TenantId" = "tenantId"; "DomainName" = "domainName"}; + TenantDetails = @{"DisplayName" = "displayName"; "TenantId" = "tenantId"; "DomainName" = "domainName" }; ModuleVersion = '1.0'; OutFolderPath = "./" OutProviderFileName = "ProviderSettingsExport" - OutJsonFileName = "ScubaResults" + OutJsonFileName = "ScubaResults"; } } 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 = @() } + It 'Throws an error when the file Path is too long' { + $LongText = "Lorem ipsum dolor sit amet, ` + consectetur adipiscing elit, sed do eiusmod tempor ` + incididunt ut labore et dolore magna aliqua. ` + Ut enim ad minim veniam, quis nostrud exercitation ` + ullamco laboris nisi ut aliquip ex ea commodo consequat. ` + Lorem ipsum dolor sit amet, consectetur adipiscing elit, + `sed do eiusmod " + $JsonParameters += @{ + ProductNames = @("aad", "teams"); + } + Mock -CommandName Join-Path { "ScubaResults" + $LongText; } + { Merge-JsonOutput @JsonParameters } | Should -Throw + $JsonParameters.ProductNames = @() + } } } } From c0c8f235a119053d96d05d227341282f34a18d69 Mon Sep 17 00:00:00 2001 From: buidav <105074908+buidav@users.noreply.github.com> Date: Sun, 17 Nov 2024 20:05:49 -0800 Subject: [PATCH 12/40] add back in accidentally removed fields in config --- PowerShell/ScubaGear/Sample-Config-Files/full_config.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/PowerShell/ScubaGear/Sample-Config-Files/full_config.yaml b/PowerShell/ScubaGear/Sample-Config-Files/full_config.yaml index 23f6e2eb08..0094aa2463 100644 --- a/PowerShell/ScubaGear/Sample-Config-Files/full_config.yaml +++ b/PowerShell/ScubaGear/Sample-Config-Files/full_config.yaml @@ -19,7 +19,9 @@ OutFolderName: M365BaselineConformance OutProviderFileName: ProviderSettingsExport OutRegoFileName: TestResults OutReportName: BaselineReports - +Organization: abcdef.example.com +AppID: 0123456789abcdef01234566789abcde +CertificateThumbprint: fedcba9876543210fedcba9876543210fedcba98 OutJsonFileName: ScubaResults OutCsvFileName: ScubaResults OutActionPlanFileName: ActionPlan From 54da4344d592663e22b928a6b0fa57b4fe38b11f Mon Sep 17 00:00:00 2001 From: buidav <105074908+buidav@users.noreply.github.com> Date: Sun, 17 Nov 2024 20:15:17 -0800 Subject: [PATCH 13/40] complete lorem ipsum --- PowerShell/ScubaGear/Modules/Orchestrator.psm1 | 1 + .../Unit/PowerShell/Orchestrator/Merge-JsonOutput.Tests.ps1 | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index 8ac109cd10..3d3559e049 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -1103,6 +1103,7 @@ function Merge-JsonOutput { throw $PathLengthErrorMessage } elseif ($AbsoluteResultsFilePathLen -gt $MAX_WINDOWS_PATH_LEN) { + # TODO decide if we need this $ReportUuid = $ReportUuid.Substring(0, $ReportUuid.Length - $CharactersToTrim) $PathLengthErrorMessage = "The GUID appended to the ScubaResults file name was truncated by $CharactersToTrim ` because ScubaGear was executed in a location where the Windows absolute file path length was longer than $($MAX_WINDOWS_PATH_LEN) 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 c272dbd5ff..3cd4f6d7ca 100644 --- a/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Merge-JsonOutput.Tests.ps1 +++ b/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Merge-JsonOutput.Tests.ps1 @@ -61,8 +61,10 @@ InModuleScope Orchestrator { incididunt ut labore et dolore magna aliqua. ` Ut enim ad minim veniam, quis nostrud exercitation ` ullamco laboris nisi ut aliquip ex ea commodo consequat. ` - Lorem ipsum dolor sit amet, consectetur adipiscing elit, - `sed do eiusmod " + Duis aute irure dolor in reprehenderit in voluptate velit ` + esse cillum dolore eu fugiat nulla pariatur. Excepteur sint ` + occaecat cupidatat non proident, sunt in culpa qui officia ` + deserunt mollit anim id est laborum" $JsonParameters += @{ ProductNames = @("aad", "teams"); } From 88c0fe2174722b8c805a192a0e88f4ccd4d7ff97 Mon Sep 17 00:00:00 2001 From: buidav <105074908+buidav@users.noreply.github.com> Date: Sun, 17 Nov 2024 20:15:41 -0800 Subject: [PATCH 14/40] todo message --- PowerShell/ScubaGear/Modules/Orchestrator.psm1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index 3d3559e049..42cc8d5451 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -1116,7 +1116,7 @@ function Merge-JsonOutput { -ErrorAction 'Stop' $ReportJson | Set-Content -Path $JsonFileName -Encoding $(Get-FileEncoding) -ErrorAction 'Stop' - # Delete the now redundant files + # Delete the now redundant ]files foreach ($File in $DeletionList) { Remove-Item $File } From 257eb88517c5fe1fc8736c6cdcd426350c133a78 Mon Sep 17 00:00:00 2001 From: buidav <105074908+buidav@users.noreply.github.com> Date: Mon, 18 Nov 2024 11:35:28 -0800 Subject: [PATCH 15/40] remove UUID truncation for now --- PowerShell/ScubaGear/Modules/Orchestrator.psm1 | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index 42cc8d5451..d2947a0ec8 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -1085,38 +1085,27 @@ function Merge-JsonOutput { $ReportJson = $ReportJson.replace("\u003e", ">") $ReportJson = $ReportJson.replace("\u0027", "'") - - # Check if the absolute final results output path is greater than the allowable windows file Path length # Trim the GUID to save some character length space if it is. $MAX_WINDOWS_PATH_LEN = 256 - $CharactersToTrim = 13 $CurrentLocation = (Get-Location) | Select-Object -ExpandProperty ProviderPath $JoinedFilePath = Join-Path -Path $CurrentLocation -ChildPath (Join-Path -Path $OutFolderPath -ChildPath "$($OutJsonFileName)_$($ReportUuid)") $AbsoluteResultsFilePathLen = ([System.IO.Path]::GetFullPath($JoinedFilePath)).Length # Throw an error if the path length is too long. - if ($AbsoluteResultsFilePathLen -gt ($MAX_WINDOWS_PATH_LEN + $CharactersToTrim)) { + if ($AbsoluteResultsFilePathLen -gt ($MAX_WINDOWS_PATH_LEN)) { $PathLengthErrorMessage = "ScubaGear was 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 Window file path limit is less than $($MAX_WINDOWS_PATH_LEN).` Your current file path length is $($AbsoluteResultsFilePathLen)" throw $PathLengthErrorMessage } - elseif ($AbsoluteResultsFilePathLen -gt $MAX_WINDOWS_PATH_LEN) { - # TODO decide if we need this - $ReportUuid = $ReportUuid.Substring(0, $ReportUuid.Length - $CharactersToTrim) - $PathLengthErrorMessage = "The GUID appended to the ScubaResults file name was truncated by $CharactersToTrim ` - because ScubaGear was executed in a location where the Windows absolute file path length was longer than $($MAX_WINDOWS_PATH_LEN) - This should not have affected the content of the final output but please proceed with caution when moving the file to other directories" - Write-Warning $PathLengthErrorMessage - } # Save the file $JsonFileName = Join-Path -Path $OutFolderPath "$($OutJsonFileName)_$($ReportUuid).json" ` -ErrorAction 'Stop' $ReportJson | Set-Content -Path $JsonFileName -Encoding $(Get-FileEncoding) -ErrorAction 'Stop' - # Delete the now redundant ]files + # Delete the now redundant files foreach ($File in $DeletionList) { Remove-Item $File } From 19c2122f33bc08b87b819bff1d93534ddee9eb83 Mon Sep 17 00:00:00 2001 From: buidav <105074908+buidav@users.noreply.github.com> Date: Mon, 18 Nov 2024 22:10:02 -0800 Subject: [PATCH 16/40] first draft --- .../ScubaGear/Modules/Orchestrator.psm1 | 108 ++++++++++++++---- .../Modules/ScubaConfig/ScubaConfig.psm1 | 1 + .../ConvertTo-ResultsCsv.Tests.ps1 | 3 + .../Orchestrator/Merge-JsonOutput.Tests.ps1 | 12 +- 4 files changed, 99 insertions(+), 25 deletions(-) diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index d2947a0ec8..e6c769e980 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -90,11 +90,13 @@ function Invoke-SCuBA { Set switch to enable report dark mode by default. .Parameter Quiet Do not launch external browser for report. + .Parameter NumberOfUUIDCharactersToTruncate + The number of characters the Report UUID appended to the end of OutJsonFileName will be truncated by. .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 +256,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 @@ -445,17 +454,19 @@ function Invoke-SCuBA { 'TenantDetails' = $TenantDetails; 'ModuleVersion' = $ModuleVersion; 'OutJsonFileName' = $ScubaConfig.OutJsonFileName; + 'NumberOfUUIDCharactersToTruncate' = $NumberOfUUIDCharactersToTruncate; } Merge-JsonOutput @JsonParams } # Craft the csv version of just the results $CsvParams = @{ - 'ProductNames' = $ScubaConfig.ProductNames; - 'Guid' = $Guid; - 'OutFolderPath' = $OutFolderPath; - 'OutJsonFileName' = $ScubaConfig.OutJsonFileName; - 'OutCsvFileName' = $ScubaConfig.OutCsvFileName; - 'OutActionPlanFileName' = $ScubaConfig.OutActionPlanFileName; + 'ProductNames' = $ScubaConfig.ProductNames; + 'Guid' = $Guid; + 'NumberOfUUIDCharactersToTruncate' = $NumberOfUUIDCharactersToTruncate; + 'OutFolderPath' = $OutFolderPath; + 'OutJsonFileName' = $ScubaConfig.OutJsonFileName; + 'OutCsvFileName' = $ScubaConfig.OutCsvFileName; + 'OutActionPlanFileName' = $ScubaConfig.OutActionPlanFileName; } ConvertTo-ResultsCsv @CsvParams } @@ -880,6 +891,11 @@ function ConvertTo-ResultsCsv { [string] $Guid, + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [int] + $NumberOfUUIDCharactersToTruncate, + [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] @@ -903,7 +919,8 @@ function ConvertTo-ResultsCsv { process { try { # Find the ScubaResults file with UUID in the file name. - $ScubaResultsFileName = Join-Path $OutFolderPath -ChildPath "$($OutJsonFileName)_$($Guid).json" + $ReportUuid = $Guid.Substring(0, $Guid.Length - $NumberOfUUIDCharactersToTruncate) + $ScubaResultsFileName = Join-Path $OutFolderPath -ChildPath "$($OutJsonFileName)_$($ReportUuid).json" if (Test-Path $ScubaResultsFileName -PathType Leaf) { # The ScubaResults file exists, no need to look for the individual json files $ScubaResults = Get-Content (Get-ChildItem $ScubaResultsFileName).FullName | ConvertFrom-Json @@ -1007,7 +1024,12 @@ function Merge-JsonOutput { [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] - $OutJsonFileName + $OutJsonFileName, + + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [int] + $NumberOfUUIDCharactersToTruncate ) process { try { @@ -1085,8 +1107,12 @@ function Merge-JsonOutput { $ReportJson = $ReportJson.replace("\u003e", ">") $ReportJson = $ReportJson.replace("\u0027", "'") + # 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 + $ReportUuid = $ReportUuid.Substring(0, $ReportUuid.Length - $NumberOfUUIDCharactersToTruncate) + # Check if the absolute final results output path is greater than the allowable windows file Path length - # Trim the GUID to save some character length space if it is. $MAX_WINDOWS_PATH_LEN = 256 $CurrentLocation = (Get-Location) | Select-Object -ExpandProperty ProviderPath $JoinedFilePath = Join-Path -Path $CurrentLocation -ChildPath (Join-Path -Path $OutFolderPath -ChildPath "$($OutJsonFileName)_$($ReportUuid)") @@ -1100,9 +1126,16 @@ function Merge-JsonOutput { throw $PathLengthErrorMessage } - # Save the file - $JsonFileName = Join-Path -Path $OutFolderPath "$($OutJsonFileName)_$($ReportUuid).json" ` + if ($ReportUuid.Length -ne 0 ) { + $JsonFileName = Join-Path -Path $OutFolderPath "$($OutJsonFileName)_$($ReportUuid).json" ` + -ErrorAction 'Stop' + } + else { + # If the user chose to truncate the entire UUID, omit it from the filename + $JsonFileName = Join-Path -Path $OutFolderPath "$($OutJsonFileName).json" ` -ErrorAction 'Stop' + } + $ReportJson | Set-Content -Path $JsonFileName -Encoding $(Get-FileEncoding) -ErrorAction 'Stop' # Delete the now redundant files @@ -1638,6 +1671,8 @@ 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 + The number of characters the Report UUID appended to the end of OutJsonFileName will be truncated by. .Example Invoke-SCuBACached Run an assessment against by default a commercial M365 Tenant against the @@ -1759,7 +1794,14 @@ function Invoke-SCuBACached { [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [switch] - $DarkMode + $DarkMode, + + [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')] + [Parameter(Mandatory = $false, ParameterSetName = 'Report')] + [ValidateNotNullOrEmpty()] + [ValidateSet(0, 13, 18, 36)] + [int] + $NumberOfUUIDCharactersToTruncate = [ScubaConfig]::ScubaDefault('DefaultNumberOfUUIDCharactersToTruncate') ) process { $ParentPath = Split-Path $PSScriptRoot -Parent @@ -1804,6 +1846,16 @@ function Invoke-SCuBACached { $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 @@ -1815,6 +1867,16 @@ function Invoke-SCuBACached { } } $TenantDetails = Get-TenantDetail -ProductNames $ProductNames -M365Environment $M365Environment + + # A new GUID needs to be generated if the provider is run + 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 + } $ProviderParams = @{ 'ProductNames' = $ProductNames; 'M365Environment' = $M365Environment; @@ -1861,12 +1923,17 @@ function Invoke-SCuBACached { Write-Warning $Warning } $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 = @{ 'ProductNames' = $ProductNames; @@ -1899,6 +1966,7 @@ function Invoke-SCuBACached { 'TenantDetails' = $TenantDetails; 'ModuleVersion' = $ModuleVersion; 'OutJsonFileName' = $OutJsonFileName; + 'NumberOfUUIDCharactersToTruncate' = $NumberOfUUIDCharactersToTruncate; } Merge-JsonOutput @JsonParams } @@ -1906,13 +1974,13 @@ function Invoke-SCuBACached { $CsvParams = @{ 'ProductNames' = $ProductNames; 'Guid' = $Guid; + 'NumberOfUUIDCharactersToTruncate' = $NumberOfUUIDCharactersToTruncate; 'OutFolderPath' = $OutFolderPath; 'OutJsonFileName' = $OutJsonFileName; '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..484f3c66c4 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", 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 a22f9090f3..44d9148a7e 100644 --- a/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/ConvertTo-ResultsCsv.Tests.ps1 +++ b/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/ConvertTo-ResultsCsv.Tests.ps1 @@ -65,6 +65,7 @@ InModuleScope Orchestrator { OutCsvFileName = "ScubaResults"; OutActionPlanFileName = "ActionPlan"; Guid = "00000000-0000-0000-0000-000000000000"; + NumberOfUUIDCharactersToTruncate = "18"; } { ConvertTo-ResultsCsv @CsvParameters} | Should -Not -Throw Should -Invoke -CommandName ConvertFrom-Json -Exactly -Times 1 @@ -84,6 +85,7 @@ InModuleScope Orchestrator { OutCsvFileName = "ScubaResults"; OutActionPlanFileName = "ActionPlan"; Guid = "00000000-0000-0000-0000-000000000000"; + NumberOfUUIDCharactersToTruncate = "18"; } { ConvertTo-ResultsCsv @CsvParameters} | Should -Not -Throw Should -Invoke -CommandName Format-PlainText -Exactly -Times 0 @@ -112,6 +114,7 @@ InModuleScope Orchestrator { OutCsvFileName = "ScubaResults"; OutActionPlanFileName = "ActionPlan"; Guid = "00000000-0000-0000-0000-000000000000"; + NumberOfUUIDCharactersToTruncate = "18"; } { ConvertTo-ResultsCsv @CsvParameters} | Should -Not -Throw Should -Invoke -CommandName Write-Warning -Exactly -Times 0 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 3cd4f6d7ca..4014958856 100644 --- a/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Merge-JsonOutput.Tests.ps1 +++ b/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Merge-JsonOutput.Tests.ps1 @@ -12,6 +12,7 @@ InModuleScope Orchestrator { "ReportSummary" = @{"Date" = "" } "Results" = @(); "timestamp_zulu" = ""; + "report_uuid" = "00000000-0000-0000-0000-000000000000" } } Mock -CommandName Add-Member {} @@ -21,11 +22,12 @@ 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" + OutJsonFileName = "ScubaResults"; + NumberOfUUIDCharactersToTruncate = 18; } } It 'Merge single result' { From 252a64812d188cd43b4e908c763b6a39b02c7389 Mon Sep 17 00:00:00 2001 From: buidav <105074908+buidav@users.noreply.github.com> Date: Tue, 19 Nov 2024 11:14:16 -0800 Subject: [PATCH 17/40] add truncation param to documenatation --- docs/configuration/parameters.md | 72 ++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 27 deletions(-) diff --git a/docs/configuration/parameters.md b/docs/configuration/parameters.md index 4fdeec1321..56459d52af 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 @@ -122,7 +122,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 +141,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. @@ -169,10 +169,10 @@ The list of acceptable values are: | Optional | Yes | | Datatype | Switch | | Default | n/a | -| Config File | No | +| Config File | No | ```powershell -# Outputs legacy ScubaGear individual JSON output +# Outputs legacy ScubaGear individual JSON output Invoke-SCuBA -ProductNames teams ` -KeepIndividualJSON ``` @@ -205,7 +205,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 +217,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 +228,7 @@ Invoke-SCuBA -ProductNames teams ` | Optional | Yes | | Datatype | String | | Default | `ActionPlan` | -| Config File | Yes | +| Config File | Yes | ```powershell @@ -246,7 +246,7 @@ Invoke-SCuBA -ProductNames teams ` | Optional | Yes | | Datatype | String | | Default | `ScubaResults` | -| Config File | Yes | +| Config File | Yes | ```powershell @@ -257,14 +257,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 +274,14 @@ Invoke-SCuBA -ProductNames teams ` ## OutJsonFileName -**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. +**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. @@ -320,7 +320,7 @@ Invoke-SCuBA -ProductNames teams ` | Optional | Yes | | Datatype | String | | Default | `ProviderSettingsExport` | -| Config File | Yes | +| Config File | Yes | ```powershell # Change the provider settings file @@ -339,7 +339,7 @@ Invoke-SCuBA -ProductNames teams ` | Optional | Yes | | Datatype | String | | Default | `TestResults` | -| Config File | Yes | +| Config File | Yes | ```powershell # Change the rego file @@ -358,7 +358,7 @@ Invoke-SCuBA -ProductNames teams ` | Optional | Yes | | Datatype | String | | Default | `BaselineReports` | -| Config File | Yes | +| Config File | Yes | ```powershell # Change the HTML report file @@ -377,7 +377,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: @@ -406,7 +406,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 @@ -414,6 +414,24 @@ Invoke-SCuBA -ProductNames teams ` -Quiet ``` +## NumberOfUUIDCharactersToTruncate + +**NumberOfUUIDCharactersToTruncate** The number of characters the Report UUID appended to the end of OutJsonFileName will be truncated by. + +| Parameter | Value | +|-------------|--------------------| +| Optional | Yes | +| Datatype | Integer | +| Default | 18 | +| Config File | Yes | + + +```powershell +# Change the output action plan file +Invoke-SCuBA -ProductNames exo ` + -NumberOfUUIDCharactersToTruncate 18 ` +``` + ## Version **Version** writes the current ScubaGear version to the console. ScubaGear will not be run. When the `Version` parameter is used, no other parameters should be included. @@ -423,7 +441,7 @@ Invoke-SCuBA -ProductNames teams ` | Optional | Yes | | Datatype | Switch | | Default | n/a | -| Config File | No | +| Config File | No | ```powershell # Check the version From 16e19fc57df95d7bfdc72d98c99ce6f3851a4307 Mon Sep 17 00:00:00 2001 From: buidav <105074908+buidav@users.noreply.github.com> Date: Tue, 19 Nov 2024 11:15:49 -0800 Subject: [PATCH 18/40] spacing --- docs/configuration/parameters.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/configuration/parameters.md b/docs/configuration/parameters.md index 56459d52af..42797832b1 100644 --- a/docs/configuration/parameters.md +++ b/docs/configuration/parameters.md @@ -425,7 +425,6 @@ Invoke-SCuBA -ProductNames teams ` | Default | 18 | | Config File | Yes | - ```powershell # Change the output action plan file Invoke-SCuBA -ProductNames exo ` From 643f68945bf3a2aeef14028e320d420a3723cddb Mon Sep 17 00:00:00 2001 From: buidav <105074908+buidav@users.noreply.github.com> Date: Tue, 19 Nov 2024 11:24:25 -0800 Subject: [PATCH 19/40] fix failing test cases; handle full truncation case --- PowerShell/ScubaGear/Modules/Orchestrator.psm1 | 8 +++++++- .../PowerShell/Orchestrator/Invoke-ScubaCached.Tests.ps1 | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index e6c769e980..5e4509ef09 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -920,7 +920,13 @@ function ConvertTo-ResultsCsv { try { # Find the ScubaResults file with UUID in the file name. $ReportUuid = $Guid.Substring(0, $Guid.Length - $NumberOfUUIDCharactersToTruncate) - $ScubaResultsFileName = Join-Path $OutFolderPath -ChildPath "$($OutJsonFileName)_$($ReportUuid).json" + if ($ReportUuid.Length -gt 0) { + $ScubaResultsFileName = Join-Path $OutFolderPath -ChildPath "$($OutJsonFileName)_$($ReportUuid).json" + } + else { + $ScubaResultsFileName = Join-Path $OutFolderPath -ChildPath "$($OutJsonFileName).json" + } + if (Test-Path $ScubaResultsFileName -PathType Leaf) { # The ScubaResults file exists, no need to look for the individual json files $ScubaResults = Get-Content (Get-ChildItem $ScubaResultsFileName).FullName | ConvertFrom-Json 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 0cbad16a3f..1eb271e867 100644 --- a/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Invoke-ScubaCached.Tests.ps1 +++ b/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Invoke-ScubaCached.Tests.ps1 @@ -33,6 +33,7 @@ InModuleScope Orchestrator { Mock -CommandName Get-ChildItem { [pscustomobject]@{"FullName"="ScubaResults.json"; "CreationTime"=[DateTime]"2024-01-01"} } + Mock -CommandName Remove-Item {} } Context 'When checking the conformance of commercial tenants' { BeforeAll { From c8b0449090bbe774d5d662913cac8ca671992eae Mon Sep 17 00:00:00 2001 From: buidav <105074908+buidav@users.noreply.github.com> Date: Tue, 19 Nov 2024 11:36:50 -0800 Subject: [PATCH 20/40] make code consistent; add code comments to describe it's purpose --- PowerShell/ScubaGear/Modules/Orchestrator.psm1 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index 5e4509ef09..34d734cb56 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -920,10 +920,13 @@ function ConvertTo-ResultsCsv { try { # Find the ScubaResults file with UUID in the file name. $ReportUuid = $Guid.Substring(0, $Guid.Length - $NumberOfUUIDCharactersToTruncate) + + # If the UUID still exists after truncation if ($ReportUuid.Length -gt 0) { $ScubaResultsFileName = Join-Path $OutFolderPath -ChildPath "$($OutJsonFileName)_$($ReportUuid).json" } else { + # Otherwise omit trying to find it from the resulting file name $ScubaResultsFileName = Join-Path $OutFolderPath -ChildPath "$($OutJsonFileName).json" } @@ -1132,7 +1135,8 @@ function Merge-JsonOutput { throw $PathLengthErrorMessage } - if ($ReportUuid.Length -ne 0 ) { + # If the UUID still exists after truncation + if ($ReportUuid.Length -gt 0 ) { $JsonFileName = Join-Path -Path $OutFolderPath "$($OutJsonFileName)_$($ReportUuid).json" ` -ErrorAction 'Stop' } From e68f055effd189af6c4b6e27a3320657ef966d88 Mon Sep 17 00:00:00 2001 From: buidav <105074908+buidav@users.noreply.github.com> Date: Tue, 19 Nov 2024 12:07:19 -0800 Subject: [PATCH 21/40] review feedback; point to additional options in the error message --- PowerShell/ScubaGear/Modules/Orchestrator.psm1 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index 34d734cb56..03e76d8079 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -1129,9 +1129,11 @@ function Merge-JsonOutput { # Throw an error if the path length is too long. if ($AbsoluteResultsFilePathLen -gt ($MAX_WINDOWS_PATH_LEN)) { - $PathLengthErrorMessage = "ScubaGear was executed in a location where the maximum file path length is greater than the allowable Windows File System limit ` + $PathLengthErrorMessage = "ScubaGear was 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 Window file path limit is less than $($MAX_WINDOWS_PATH_LEN).` - Your current file path length is $($AbsoluteResultsFilePathLen)" + Your current file path length is $($AbsoluteResultsFilePathLen). + 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 } From 8e03b7752bd2679cbaaea0d0ea96f75c7004bf85 Mon Sep 17 00:00:00 2001 From: buidav <105074908+buidav@users.noreply.github.com> Date: Tue, 19 Nov 2024 15:42:02 -0800 Subject: [PATCH 22/40] PR Review: Fix absolute path check; fix config file override --- .../ScubaGear/Modules/Orchestrator.psm1 | 24 +++++++++++++++---- .../Modules/ScubaConfig/ScubaConfig.psm1 | 4 ++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index 03e76d8079..bd534b51d7 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -299,6 +299,7 @@ function Invoke-SCuBA { 'OutJsonFileName' = $OutJsonFileName 'OutCsvFileName' = $OutCsvFileName 'OutActionPlanFileName' = $OutActionPlanFileName + 'NumberOfUUIDCharactersToTruncate' = $NumberOfUUIDCharactersToTruncate } $ScubaConfig = New-Object -Type PSObject -Property $ProvidedParameters @@ -454,7 +455,7 @@ function Invoke-SCuBA { 'TenantDetails' = $TenantDetails; 'ModuleVersion' = $ModuleVersion; 'OutJsonFileName' = $ScubaConfig.OutJsonFileName; - 'NumberOfUUIDCharactersToTruncate' = $NumberOfUUIDCharactersToTruncate; + 'NumberOfUUIDCharactersToTruncate' = $ScubaConfig.NumberOfUUIDCharactersToTruncate; } Merge-JsonOutput @JsonParams } @@ -462,7 +463,7 @@ function Invoke-SCuBA { $CsvParams = @{ 'ProductNames' = $ScubaConfig.ProductNames; 'Guid' = $Guid; - 'NumberOfUUIDCharactersToTruncate' = $NumberOfUUIDCharactersToTruncate; + 'NumberOfUUIDCharactersToTruncate' = $ScubaConfig.NumberOfUUIDCharactersToTruncate; 'OutFolderPath' = $OutFolderPath; 'OutJsonFileName' = $ScubaConfig.OutJsonFileName; 'OutCsvFileName' = $ScubaConfig.OutCsvFileName; @@ -1123,9 +1124,22 @@ function Merge-JsonOutput { # Check if the absolute final results output path is greater than the allowable windows file Path length $MAX_WINDOWS_PATH_LEN = 256 - $CurrentLocation = (Get-Location) | Select-Object -ExpandProperty ProviderPath - $JoinedFilePath = Join-Path -Path $CurrentLocation -ChildPath (Join-Path -Path $OutFolderPath -ChildPath "$($OutJsonFileName)_$($ReportUuid)") - $AbsoluteResultsFilePathLen = ([System.IO.Path]::GetFullPath($JoinedFilePath)).Length + $CurrentOutputPath = Join-Path -Path $OutFolderPath -ChildPath "$($OutJsonFileName)_$($ReportUuid)" + + $AbsoluteResultsFilePathLen = 0 + if ([System.IO.Path]::IsPathRooted($CurrentOutputPath)) { + Write-Host $CurrentOutputPath + Write-Host [System.IO.Path]::GetFullPath($CurrentOutputPath) + $AbsoluteResultsFilePathLen = ([System.IO.Path]::GetFullPath($CurrentOutputPath)).Length + } + else { + $CurrentLocation = (Get-Location) | Select-Object -ExpandProperty ProviderPath + $JoinedFilePath = Join-Path -Path $CurrentLocation -ChildPath $CurrentOutputPath + $AbsoluteResultsFilePathLen = ([System.IO.Path]::GetFullPath($JoinedFilePath)).Length + Write-Host $JoinedFilePath + Write-Host [System.IO.Path]::GetFullPath($JoinedFilePath) + } + # Throw an error if the path length is too long. if ($AbsoluteResultsFilePathLen -gt ($MAX_WINDOWS_PATH_LEN)) { diff --git a/PowerShell/ScubaGear/Modules/ScubaConfig/ScubaConfig.psm1 b/PowerShell/ScubaGear/Modules/ScubaConfig/ScubaConfig.psm1 index 484f3c66c4..93c368e426 100644 --- a/PowerShell/ScubaGear/Modules/ScubaConfig/ScubaConfig.psm1 +++ b/PowerShell/ScubaGear/Modules/ScubaConfig/ScubaConfig.psm1 @@ -157,6 +157,10 @@ class ScubaConfig { $this.Configuration.OutActionPlanFileName = [ScubaConfig]::ScubaDefault('DefaultOutActionPlanFileName') } + if (-Not $this.Configuration.NumberOfUUIDCharactersToTruncate){ + $this.Configuration.NumberOfUUIDCharactersToTruncate = [ScubaConfig]::ScubaDefault('DefaultNumberOfUUIDCharactersToTruncate') + } + return } From 011751dced6e4504e486ad84090130e73a346187 Mon Sep 17 00:00:00 2001 From: buidav <105074908+buidav@users.noreply.github.com> Date: Tue, 19 Nov 2024 15:57:45 -0800 Subject: [PATCH 23/40] review feedback; move new parameter in alphabetical order in docs --- .../ScubaGear/Modules/Orchestrator.psm1 | 7 +- docs/configuration/parameters.md | 68 +++++++++++-------- 2 files changed, 42 insertions(+), 33 deletions(-) diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index bd534b51d7..39c3c1e625 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -91,7 +91,8 @@ function Invoke-SCuBA { .Parameter Quiet Do not launch external browser for report. .Parameter NumberOfUUIDCharactersToTruncate - The number of characters the Report UUID appended to the end of OutJsonFileName will be truncated by. + NumberOfUUIDCharactersToTruncate controls how many characters will be trimmed 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 @@ -1128,16 +1129,12 @@ function Merge-JsonOutput { $AbsoluteResultsFilePathLen = 0 if ([System.IO.Path]::IsPathRooted($CurrentOutputPath)) { - Write-Host $CurrentOutputPath - Write-Host [System.IO.Path]::GetFullPath($CurrentOutputPath) $AbsoluteResultsFilePathLen = ([System.IO.Path]::GetFullPath($CurrentOutputPath)).Length } else { $CurrentLocation = (Get-Location) | Select-Object -ExpandProperty ProviderPath $JoinedFilePath = Join-Path -Path $CurrentLocation -ChildPath $CurrentOutputPath $AbsoluteResultsFilePathLen = ([System.IO.Path]::GetFullPath($JoinedFilePath)).Length - Write-Host $JoinedFilePath - Write-Host [System.IO.Path]::GetFullPath($JoinedFilePath) } diff --git a/docs/configuration/parameters.md b/docs/configuration/parameters.md index 42797832b1..eaa7eaed74 100644 --- a/docs/configuration/parameters.md +++ b/docs/configuration/parameters.md @@ -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. @@ -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** NumberOfUUIDCharactersToTruncate controls how many characters will be trimmed 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 | 0 | +| Remove one octet of the appended UUID | 13 | +| Remove two octet 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 @@ -414,23 +443,6 @@ Invoke-SCuBA -ProductNames teams ` -Quiet ``` -## NumberOfUUIDCharactersToTruncate - -**NumberOfUUIDCharactersToTruncate** The number of characters the Report UUID appended to the end of OutJsonFileName will be truncated by. - -| Parameter | Value | -|-------------|--------------------| -| Optional | Yes | -| Datatype | Integer | -| Default | 18 | -| Config File | Yes | - -```powershell -# Change the output action plan file -Invoke-SCuBA -ProductNames exo ` - -NumberOfUUIDCharactersToTruncate 18 ` -``` - ## Version **Version** writes the current ScubaGear version to the console. ScubaGear will not be run. When the `Version` parameter is used, no other parameters should be included. From d78b0a5ddbf349be6a7eb276cfb652a43c3e9e08 Mon Sep 17 00:00:00 2001 From: buidav <105074908+buidav@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:00:41 -0800 Subject: [PATCH 24/40] keep documentation consistent --- PowerShell/ScubaGear/Modules/Orchestrator.psm1 | 5 +++-- docs/configuration/parameters.md | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index 39c3c1e625..ce6c19ec58 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -91,7 +91,7 @@ function Invoke-SCuBA { .Parameter Quiet Do not launch external browser for report. .Parameter NumberOfUUIDCharactersToTruncate - NumberOfUUIDCharactersToTruncate controls how many characters will be trimmed from the report UUID when appended to the end of OutJsonFileName. + 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 @@ -1695,7 +1695,8 @@ function Invoke-SCuBACached { .Parameter DarkMode Set switch to enable report dark mode by default. .Parameter NumberOfUUIDCharactersToTruncate - The number of characters the Report UUID appended to the end of OutJsonFileName will be truncated by. + 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 diff --git a/docs/configuration/parameters.md b/docs/configuration/parameters.md index eaa7eaed74..806cf84b5a 100644 --- a/docs/configuration/parameters.md +++ b/docs/configuration/parameters.md @@ -181,7 +181,7 @@ The list of acceptable values are: ## NumberOfUUIDCharactersToTruncate -**NumberOfUUIDCharactersToTruncate** NumberOfUUIDCharactersToTruncate controls how many characters will be trimmed from the report UUID when appended to the end of OutJsonFileName. +**NumberOfUUIDCharactersToTruncate** NumberOfUUIDCharactersToTruncate controls how many characters will be truncated from the report UUID when appended to the end of OutJsonFileName. | Parameter | Value | |-------------|--------------------| From 2d3481dfc26b068c8c613ebf39981c591d3a5d27 Mon Sep 17 00:00:00 2001 From: buidav <105074908+buidav@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:01:28 -0800 Subject: [PATCH 25/40] remove configuration paramset from scubacached --- PowerShell/ScubaGear/Modules/Orchestrator.psm1 | 1 - 1 file changed, 1 deletion(-) diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index ce6c19ec58..a8446284fd 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -1820,7 +1820,6 @@ function Invoke-SCuBACached { [switch] $DarkMode, - [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')] [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [ValidateSet(0, 13, 18, 36)] From 50fbb4a81904bfaf8ce5d1a84f52f3271fe981eb Mon Sep 17 00:00:00 2001 From: buidav <105074908+buidav@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:05:34 -0800 Subject: [PATCH 26/40] code comments for the new edge case --- PowerShell/ScubaGear/Modules/Orchestrator.psm1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index a8446284fd..32fb6817c0 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -1126,12 +1126,13 @@ function Merge-JsonOutput { # Check if the absolute final results output path is greater than the allowable windows file Path length $MAX_WINDOWS_PATH_LEN = 256 $CurrentOutputPath = Join-Path -Path $OutFolderPath -ChildPath "$($OutJsonFileName)_$($ReportUuid)" - $AbsoluteResultsFilePathLen = 0 if ([System.IO.Path]::IsPathRooted($CurrentOutputPath)) { + # If the current output path is absolute $AbsoluteResultsFilePathLen = ([System.IO.Path]::GetFullPath($CurrentOutputPath)).Length } else { + # If the current output path is relative $CurrentLocation = (Get-Location) | Select-Object -ExpandProperty ProviderPath $JoinedFilePath = Join-Path -Path $CurrentLocation -ChildPath $CurrentOutputPath $AbsoluteResultsFilePathLen = ([System.IO.Path]::GetFullPath($JoinedFilePath)).Length From 0d4b78ef7e7f9cd0db29317776ea5cd26637fdf4 Mon Sep 17 00:00:00 2001 From: Alden Hilton Date: Tue, 19 Nov 2024 16:21:26 -0800 Subject: [PATCH 27/40] Remove OBE unit test --- .../ConvertTo-ResultsCsv.Tests.ps1 | 28 ------------------- 1 file changed, 28 deletions(-) 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 44d9148a7e..9468c5d17e 100644 --- a/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/ConvertTo-ResultsCsv.Tests.ps1 +++ b/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/ConvertTo-ResultsCsv.Tests.ps1 @@ -91,34 +91,6 @@ InModuleScope Orchestrator { Should -Invoke -CommandName Format-PlainText -Exactly -Times 0 Should -Invoke -CommandName Write-Warning -Exactly -Times 1 } - - It 'Handles multiple ScubaResults*.json file names' { - # If this function is called from Invoke-ScubaCached, 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. - Mock -CommandName Get-ChildItem { @( - [pscustomobject]@{"FullName"="ScubaResults_00000000-0000-0000-0000-000000000000.json"; "CreationTime"=[DateTime]"2024-01-01"} - ) } - - Mock -CommandName Get-Content { - if ($Path -ne "ScubaResults_00000000-0000-0000-0000-000000000000.json") { - # Should be the exact file name, throw if not - throw - } - } - - $CsvParameters = @{ - ProductNames = @("exo"); - OutFolderPath = "."; - OutJsonFileName = "ScubaResults"; - OutCsvFileName = "ScubaResults"; - OutActionPlanFileName = "ActionPlan"; - Guid = "00000000-0000-0000-0000-000000000000"; - NumberOfUUIDCharactersToTruncate = "18"; - } - { ConvertTo-ResultsCsv @CsvParameters} | Should -Not -Throw - Should -Invoke -CommandName Write-Warning -Exactly -Times 0 - } } } From 5d7cfee56f2d4cf6af553e5ffd860a759ce4f20f Mon Sep 17 00:00:00 2001 From: Alden Hilton Date: Tue, 19 Nov 2024 16:22:57 -0800 Subject: [PATCH 28/40] Remove duplicate word --- docs/configuration/parameters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/parameters.md b/docs/configuration/parameters.md index 806cf84b5a..c764531399 100644 --- a/docs/configuration/parameters.md +++ b/docs/configuration/parameters.md @@ -181,7 +181,7 @@ The list of acceptable values are: ## NumberOfUUIDCharactersToTruncate -**NumberOfUUIDCharactersToTruncate** NumberOfUUIDCharactersToTruncate controls how many characters will be truncated from the report UUID when appended to the end of OutJsonFileName. +**NumberOfUUIDCharactersToTruncate** controls how many characters will be truncated from the report UUID when appended to the end of **OutJsonFileName**. | Parameter | Value | |-------------|--------------------| From c38d1f09bbb9dafee1a37a617b6f8712822174ef Mon Sep 17 00:00:00 2001 From: buidav <105074908+buidav@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:22:08 -0800 Subject: [PATCH 29/40] fix typos, wording and formatting in config --- docs/configuration/parameters.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/configuration/parameters.md b/docs/configuration/parameters.md index c764531399..d784b63beb 100644 --- a/docs/configuration/parameters.md +++ b/docs/configuration/parameters.md @@ -193,12 +193,12 @@ The list of acceptable values are: The list of acceptable values are: -| Description | Value | -|---------------------------------------|------------| -| Do no truncation | 0 | -| Remove one octet of the appended UUID | 13 | -| Remove two octet of the appended UUID | 18 | -| Remove the appended UUID completely | 36 | +| 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 # Truncate the UUID at the end of OutJsonFileName by 18 characters From 9022b72d2c4d35adc75f2968d7703ebd0e929f61 Mon Sep 17 00:00:00 2001 From: Alden Hilton Date: Wed, 20 Nov 2024 12:30:27 -0800 Subject: [PATCH 30/40] Refactor truncation logic into own function --- .../ScubaGear/Modules/Orchestrator.psm1 | 146 ++++++++++-------- .../Get-FullOutJsonName.Tests.ps1 | 43 ++++++ 2 files changed, 126 insertions(+), 63 deletions(-) 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 32fb6817c0..f420abdcdd 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -219,6 +219,7 @@ function Invoke-SCuBA { [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')] [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] + # [ValidatePattern('^[a-zA-Z0-9]+$')] [string] $OutJsonFileName = [ScubaConfig]::ScubaDefault('DefaultOutJsonFileName'), @@ -447,28 +448,34 @@ 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; - 'NumberOfUUIDCharactersToTruncate' = $ScubaConfig.NumberOfUUIDCharactersToTruncate; + '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; - 'Guid' = $Guid; - 'NumberOfUUIDCharactersToTruncate' = $ScubaConfig.NumberOfUUIDCharactersToTruncate; - 'OutFolderPath' = $OutFolderPath; - 'OutJsonFileName' = $ScubaConfig.OutJsonFileName; - 'OutCsvFileName' = $ScubaConfig.OutCsvFileName; - 'OutActionPlanFileName' = $ScubaConfig.OutActionPlanFileName; + 'ProductNames' = $ScubaConfig.ProductNames; + 'OutFolderPath' = $OutFolderPath; + 'FullScubaResultsName' = $FullScubaResultsName; + 'OutCsvFileName' = $ScubaConfig.OutCsvFileName; + 'OutActionPlanFileName' = $ScubaConfig.OutActionPlanFileName; } ConvertTo-ResultsCsv @CsvParams } @@ -873,10 +880,10 @@ function Format-PlainText { } } -function ConvertTo-ResultsCsv { +function Get-FullOutJsonName { <# .Description - This function converts the controls inside the Results section of the json output to a csv. + This function determines the full file name of the SCuBA results file. .Functionality Internal #> @@ -884,9 +891,8 @@ function ConvertTo-ResultsCsv { param( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] - [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", '*', IgnoreCase = $false)] - [string[]] - $ProductNames, + [string] + $OutJsonFileName, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] @@ -896,7 +902,41 @@ function ConvertTo-ResultsCsv { [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [int] - $NumberOfUUIDCharactersToTruncate, + $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 + This function converts the controls inside the Results section of the json output to a csv. + .Functionality + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", '*', IgnoreCase = $false)] + [string[]] + $ProductNames, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] @@ -906,7 +946,7 @@ function ConvertTo-ResultsCsv { [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] - $OutJsonFileName, + $FullScubaResultsName, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] @@ -920,21 +960,11 @@ function ConvertTo-ResultsCsv { ) process { try { - # Find the ScubaResults file with UUID in the file name. - $ReportUuid = $Guid.Substring(0, $Guid.Length - $NumberOfUUIDCharactersToTruncate) - - # If the UUID still exists after truncation - if ($ReportUuid.Length -gt 0) { - $ScubaResultsFileName = Join-Path $OutFolderPath -ChildPath "$($OutJsonFileName)_$($ReportUuid).json" - } - else { - # Otherwise omit trying to find it from the resulting file name - $ScubaResultsFileName = Join-Path $OutFolderPath -ChildPath "$($OutJsonFileName).json" - } + $ScubaResultsPath = Join-Path $OutFolderPath -ChildPath $FullScubaResultsName - if (Test-Path $ScubaResultsFileName -PathType Leaf) { + if (Test-Path $ScubaResultsPath -PathType Leaf) { # The ScubaResults file exists, no need to look for the individual json files - $ScubaResults = Get-Content (Get-ChildItem $ScubaResultsFileName).FullName | 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 @@ -1035,12 +1065,12 @@ function Merge-JsonOutput { [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] - $OutJsonFileName, + $FullScubaResultsName, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] - [int] - $NumberOfUUIDCharactersToTruncate + [string] + $Guid ) process { try { @@ -1053,7 +1083,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 = @() @@ -1077,7 +1106,7 @@ function Merge-JsonOutput { "Tool" = "ScubaGear"; "ToolVersion" = $ModuleVersion; "TimestampZulu" = $TimestampZulu; - "ReportUUID" = $ReportUuid; + "ReportUUID" = $Guid; } @@ -1118,14 +1147,9 @@ function Merge-JsonOutput { $ReportJson = $ReportJson.replace("\u003e", ">") $ReportJson = $ReportJson.replace("\u0027", "'") - # 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 - $ReportUuid = $ReportUuid.Substring(0, $ReportUuid.Length - $NumberOfUUIDCharactersToTruncate) - # Check if the absolute final results output path is greater than the allowable windows file Path length $MAX_WINDOWS_PATH_LEN = 256 - $CurrentOutputPath = Join-Path -Path $OutFolderPath -ChildPath "$($OutJsonFileName)_$($ReportUuid)" + $CurrentOutputPath = Join-Path -Path $OutFolderPath -ChildPath $FullScubaResultsName $AbsoluteResultsFilePathLen = 0 if ([System.IO.Path]::IsPathRooted($CurrentOutputPath)) { # If the current output path is absolute @@ -1149,18 +1173,8 @@ function Merge-JsonOutput { throw $PathLengthErrorMessage } - # If the UUID still exists after truncation - if ($ReportUuid.Length -gt 0 ) { - $JsonFileName = Join-Path -Path $OutFolderPath "$($OutJsonFileName)_$($ReportUuid).json" ` - -ErrorAction 'Stop' - } - else { - # If the user chose to truncate the entire UUID, omit it from the filename - $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) { @@ -1169,7 +1183,8 @@ function Merge-JsonOutput { } catch { $MergeJsonErrorMessage = "Fatal Error involving the Json reports aggregation. ` - Ending ScubaGear execution. See the exception message for more details: $($_)" + Ending ScubaGear execution. See the exception message for more details: $($_) ` + $($_.ScriptStackTrace)" throw $MergeJsonErrorMessage } } @@ -1981,6 +1996,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 = @{ @@ -1989,18 +2011,16 @@ function Invoke-SCuBACached { 'OutProviderFileName' = $OutProviderFileName; 'TenantDetails' = $TenantDetails; 'ModuleVersion' = $ModuleVersion; - 'OutJsonFileName' = $OutJsonFileName; - 'NumberOfUUIDCharactersToTruncate' = $NumberOfUUIDCharactersToTruncate; + 'FullScubaResultsName' = $FullScubaResultsName; + 'Guid' = $Guid; } Merge-JsonOutput @JsonParams } # Craft the csv version of just the results $CsvParams = @{ 'ProductNames' = $ProductNames; - 'Guid' = $Guid; - 'NumberOfUUIDCharactersToTruncate' = $NumberOfUUIDCharactersToTruncate; 'OutFolderPath' = $OutFolderPath; - 'OutJsonFileName' = $OutJsonFileName; + 'FullScubaResultsName' = $FullScubaResultsName; 'OutCsvFileName' = $OutCsvFileName; 'OutActionPlanFileName' = $OutActionPlanFileName; } 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 From f2fcb7622bfbc402ab357f5a922631de2300f694 Mon Sep 17 00:00:00 2001 From: buidav <105074908+buidav@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:37:25 -0800 Subject: [PATCH 31/40] rm duplicate text from PowerShell as well --- PowerShell/ScubaGear/Modules/Orchestrator.psm1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index f420abdcdd..8833d783d0 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -91,7 +91,7 @@ function Invoke-SCuBA { .Parameter Quiet Do not launch external browser for report. .Parameter NumberOfUUIDCharactersToTruncate - NumberOfUUIDCharactersToTruncate controls how many characters will be truncated from the report UUID when appended to the end of OutJsonFileName. + 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 @@ -1711,7 +1711,7 @@ function Invoke-SCuBACached { .Parameter DarkMode Set switch to enable report dark mode by default. .Parameter NumberOfUUIDCharactersToTruncate - NumberOfUUIDCharactersToTruncate controls how many characters will be truncated from the report UUID when appended to the end of OutJsonFileName. + 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 From 9f240fb9c993f8eb44482a7b349f6170cdac29f8 Mon Sep 17 00:00:00 2001 From: buidav <105074908+buidav@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:38:04 -0800 Subject: [PATCH 32/40] captialize --- PowerShell/ScubaGear/Modules/Orchestrator.psm1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index 8833d783d0..37a4438ced 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -91,7 +91,7 @@ function Invoke-SCuBA { .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. + 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 @@ -1711,7 +1711,7 @@ function Invoke-SCuBACached { .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. + 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 From 04d0bd3bde26d8afa3504ec128b474cf49a74dcd Mon Sep 17 00:00:00 2001 From: buidav <105074908+buidav@users.noreply.github.com> Date: Wed, 20 Nov 2024 13:44:09 -0800 Subject: [PATCH 33/40] remove long path check let the set-content naturally error out --- .../ScubaGear/Modules/Orchestrator.psm1 | 28 +------------------ 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index 37a4438ced..c2bb98f193 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -1147,32 +1147,6 @@ function Merge-JsonOutput { $ReportJson = $ReportJson.replace("\u003e", ">") $ReportJson = $ReportJson.replace("\u0027", "'") - # Check if the absolute final results output path is greater than the allowable windows file Path length - $MAX_WINDOWS_PATH_LEN = 256 - $CurrentOutputPath = Join-Path -Path $OutFolderPath -ChildPath $FullScubaResultsName - $AbsoluteResultsFilePathLen = 0 - if ([System.IO.Path]::IsPathRooted($CurrentOutputPath)) { - # If the current output path is absolute - $AbsoluteResultsFilePathLen = ([System.IO.Path]::GetFullPath($CurrentOutputPath)).Length - } - else { - # If the current output path is relative - $CurrentLocation = (Get-Location) | Select-Object -ExpandProperty ProviderPath - $JoinedFilePath = Join-Path -Path $CurrentLocation -ChildPath $CurrentOutputPath - $AbsoluteResultsFilePathLen = ([System.IO.Path]::GetFullPath($JoinedFilePath)).Length - } - - - # Throw an error if the path length is too long. - if ($AbsoluteResultsFilePathLen -gt ($MAX_WINDOWS_PATH_LEN)) { - $PathLengthErrorMessage = "ScubaGear was 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 Window file path limit is less than $($MAX_WINDOWS_PATH_LEN).` - Your current file path length is $($AbsoluteResultsFilePathLen). - 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 - } - $ScubaResultsPath = Join-Path $OutFolderPath -ChildPath $FullScubaResultsName -ErrorAction 'Stop' $ReportJson | Set-Content -Path $ScubaResultsPath -Encoding $(Get-FileEncoding) -ErrorAction 'Stop' @@ -1881,7 +1855,7 @@ function Invoke-SCuBACached { 'BoundParameters' = $PSBoundParameters; } - # Rego Testing failsafe + # In $TenantDetails = @{"DisplayName"="Rego Testing";} $TenantDetails = $TenantDetails | ConvertTo-Json -Depth 3 if ($ExportProvider) { From d4008d38dba8361c7fffe05e0ab71c45936392ad Mon Sep 17 00:00:00 2001 From: buidav <105074908+buidav@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:12:07 -0800 Subject: [PATCH 34/40] add long path error check within the catch block --- .../ScubaGear/Modules/Orchestrator.psm1 | 50 ++++++++----------- .../ConvertTo-ResultsCsv.Tests.ps1 | 5 +- .../Orchestrator/Merge-JsonOutput.Tests.ps1 | 17 ------- 3 files changed, 23 insertions(+), 49 deletions(-) diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index c2bb98f193..a4713001fd 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -388,14 +388,7 @@ function Invoke-SCuBA { # Generate the GUID for the JSON # TODO Stick the GUID within the ScubaConfig object to clean up parameter bloat - 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' try { # Provider Execution @@ -1156,10 +1149,20 @@ function Merge-JsonOutput { } } catch { - $MergeJsonErrorMessage = "Fatal Error involving the Json reports aggregation. ` - Ending ScubaGear execution. See the exception message for more details: $($_) ` - $($_.ScriptStackTrace)" - 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: $($_) ` + $($_.ScriptStackTrace)" + throw $MergeJsonErrorMessage + } } } } @@ -1855,9 +1858,11 @@ function Invoke-SCuBACached { 'BoundParameters' = $PSBoundParameters; } - # In + # 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 @@ -1882,14 +1887,8 @@ function Invoke-SCuBACached { $TenantDetails = Get-TenantDetail -ProductNames $ProductNames -M365Environment $M365Environment # A new GUID needs to be generated if the provider is run - 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' + $ProviderParams = @{ 'ProductNames' = $ProductNames; 'M365Environment' = $M365Environment; @@ -1927,14 +1926,7 @@ 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 } else { 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 9468c5d17e..6d6224145b 100644 --- a/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/ConvertTo-ResultsCsv.Tests.ps1 +++ b/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/ConvertTo-ResultsCsv.Tests.ps1 @@ -61,7 +61,7 @@ InModuleScope Orchestrator { $CsvParameters = @{ ProductNames = @("exo", "aad"); OutFolderPath = "."; - OutJsonFileName = "ScubaResults"; + FullScubaResultsName = "ScubaResults"; OutCsvFileName = "ScubaResults"; OutActionPlanFileName = "ActionPlan"; Guid = "00000000-0000-0000-0000-000000000000"; @@ -81,10 +81,9 @@ InModuleScope Orchestrator { $CsvParameters = @{ ProductNames = @("exo", "aad"); OutFolderPath = "."; - OutJsonFileName = "ScubaResults"; + FullScubaResultsName = "ScubaResults"; OutCsvFileName = "ScubaResults"; OutActionPlanFileName = "ActionPlan"; - Guid = "00000000-0000-0000-0000-000000000000"; NumberOfUUIDCharactersToTruncate = "18"; } { ConvertTo-ResultsCsv @CsvParameters} | Should -Not -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 4014958856..c1bde9b16e 100644 --- a/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Merge-JsonOutput.Tests.ps1 +++ b/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Merge-JsonOutput.Tests.ps1 @@ -57,23 +57,6 @@ InModuleScope Orchestrator { Should -Invoke -CommandName Remove-Item -Exactly -Times 3 $JsonParameters.ProductNames = @() } - It 'Throws an error when the file Path is too long' { - $LongText = "Lorem ipsum dolor sit amet, ` - consectetur adipiscing elit, sed do eiusmod tempor ` - incididunt ut labore et dolore magna aliqua. ` - Ut enim ad minim veniam, quis nostrud exercitation ` - ullamco laboris nisi ut aliquip ex ea commodo consequat. ` - Duis aute irure dolor in reprehenderit in voluptate velit ` - esse cillum dolore eu fugiat nulla pariatur. Excepteur sint ` - occaecat cupidatat non proident, sunt in culpa qui officia ` - deserunt mollit anim id est laborum" - $JsonParameters += @{ - ProductNames = @("aad", "teams"); - } - Mock -CommandName Join-Path { "ScubaResults" + $LongText; } - { Merge-JsonOutput @JsonParameters } | Should -Throw - $JsonParameters.ProductNames = @() - } } } } From 01b9ecb70f99e6bd659bb40468968734aaed15cc Mon Sep 17 00:00:00 2001 From: buidav <105074908+buidav@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:01:56 -0800 Subject: [PATCH 35/40] remove todo --- PowerShell/ScubaGear/Modules/Orchestrator.psm1 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index a4713001fd..355205227c 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -386,8 +386,7 @@ function Invoke-SCuBA { # Tenant Metadata for the Report $TenantDetails = Get-TenantDetail -ProductNames $ScubaConfig.ProductNames -M365Environment $ScubaConfig.M365Environment - # Generate the GUID for the JSON - # TODO Stick the GUID within the ScubaConfig object to clean up parameter bloat + # Generate a GUID to uniquely identify the output JSON $Guid = New-Guid -ErrorAction 'Stop' try { From 2c562e940aa27c121552b34e32cb7df23f77ed95 Mon Sep 17 00:00:00 2001 From: Alden Hilton Date: Wed, 20 Nov 2024 15:27:24 -0800 Subject: [PATCH 36/40] Add UUID to mock data for cached tests --- .../PowerShell/Orchestrator/Invoke-ScubaCached.Tests.ps1 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 1eb271e867..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,13 +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 { @@ -130,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 From a1f7f80178ac1e59b95fcee1fd725bbbb9881e4c Mon Sep 17 00:00:00 2001 From: Alden Hilton Date: Wed, 20 Nov 2024 15:38:26 -0800 Subject: [PATCH 37/40] Fix unit tests --- .../Orchestrator/ConvertTo-ResultsCsv.Tests.ps1 | 3 --- .../PowerShell/Orchestrator/Merge-JsonOutput.Tests.ps1 | 8 ++++---- 2 files changed, 4 insertions(+), 7 deletions(-) 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 6d6224145b..3d1bf47fbd 100644 --- a/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/ConvertTo-ResultsCsv.Tests.ps1 +++ b/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/ConvertTo-ResultsCsv.Tests.ps1 @@ -64,8 +64,6 @@ InModuleScope Orchestrator { FullScubaResultsName = "ScubaResults"; OutCsvFileName = "ScubaResults"; OutActionPlanFileName = "ActionPlan"; - Guid = "00000000-0000-0000-0000-000000000000"; - NumberOfUUIDCharactersToTruncate = "18"; } { ConvertTo-ResultsCsv @CsvParameters} | Should -Not -Throw Should -Invoke -CommandName ConvertFrom-Json -Exactly -Times 1 @@ -84,7 +82,6 @@ InModuleScope Orchestrator { FullScubaResultsName = "ScubaResults"; OutCsvFileName = "ScubaResults"; OutActionPlanFileName = "ActionPlan"; - NumberOfUUIDCharactersToTruncate = "18"; } { ConvertTo-ResultsCsv @CsvParameters} | Should -Not -Throw Should -Invoke -CommandName Format-PlainText -Exactly -Times 0 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 c1bde9b16e..fb1dfdbe1e 100644 --- a/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Merge-JsonOutput.Tests.ps1 +++ b/PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Merge-JsonOutput.Tests.ps1 @@ -24,10 +24,10 @@ InModuleScope Orchestrator { $JsonParameters = @{ TenantDetails = @{"DisplayName" = "displayName"; "TenantId" = "tenantId"; "DomainName" = "domainName" }; ModuleVersion = '1.0'; - OutFolderPath = "./" - OutProviderFileName = "ProviderSettingsExport" - OutJsonFileName = "ScubaResults"; - NumberOfUUIDCharactersToTruncate = 18; + OutFolderPath = "./"; + OutProviderFileName = "ProviderSettingsExport"; + FullScubaResultsName = "ScubaResults.json"; + Guid = "00000000-0000-0000-0000-000000000000"; } } It 'Merge single result' { From a55387f39a5eb4563208e93076c6db0345809c35 Mon Sep 17 00:00:00 2001 From: David Bui <105074908+buidav@users.noreply.github.com> Date: Wed, 20 Nov 2024 16:56:59 -0800 Subject: [PATCH 38/40] Remove commented out validation code. Co-authored-by: mitchelbaker-cisa <149098823+mitchelbaker-cisa@users.noreply.github.com> --- PowerShell/ScubaGear/Modules/Orchestrator.psm1 | 1 - 1 file changed, 1 deletion(-) diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index 355205227c..5588767237 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -219,7 +219,6 @@ function Invoke-SCuBA { [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')] [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] - # [ValidatePattern('^[a-zA-Z0-9]+$')] [string] $OutJsonFileName = [ScubaConfig]::ScubaDefault('DefaultOutJsonFileName'), From 723da01ffc84d41e4fdc6d19116723b8c5213089 Mon Sep 17 00:00:00 2001 From: buidav <105074908+buidav@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:18:42 -0800 Subject: [PATCH 39/40] add validation set to check invalid config file parameter --- PowerShell/ScubaGear/Modules/Orchestrator.psm1 | 1 + 1 file changed, 1 insertion(+) diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index 5588767237..994fd3c036 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -892,6 +892,7 @@ function Get-FullOutJsonName { [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] + [ValidateSet(0, 13, 18, 36)] [int] $NumberOfUUIDCharactersToTruncate ) From 031a26fd770d5b2a026e5444bb167bdba1745234 Mon Sep 17 00:00:00 2001 From: Alden Hilton Date: Thu, 21 Nov 2024 08:24:41 -0800 Subject: [PATCH 40/40] Remove stacktrace --- PowerShell/ScubaGear/Modules/Orchestrator.psm1 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 index 994fd3c036..66f2cb4f03 100644 --- a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -1158,8 +1158,7 @@ function Merge-JsonOutput { } else { $MergeJsonErrorMessage = "Fatal Error involving the Json reports aggregation. ` - Ending ScubaGear execution. See the exception message for more details: $($_) ` - $($_.ScriptStackTrace)" + Ending ScubaGear execution. See the exception message for more details: $($_)" throw $MergeJsonErrorMessage } }