Modules/Orchestrator.psm1
using module 'ScubaConfig\ScubaConfig.psm1' function Invoke-SCuBA { <# .SYNOPSIS Execute the SCuBAGear tool security baselines for specified M365 products. .Description This is the main function that runs the Providers, Rego, and Report creation all in one PowerShell script call. .Parameter ProductNames A list of one or more M365 shortened product names that the tool will assess when it is executed. Acceptable product name values are listed below. To assess Azure Active Directory you would enter the value aad. To assess Exchange Online you would enter exo and so forth. - Azure Active Directory: aad - Defender for Office 365: defender - Exchange Online: exo - MS Power Platform: powerplatform - SharePoint Online: sharepoint - MS Teams: teams. Use '*' to run all baselines. .Parameter M365Environment This parameter is used to authenticate to the different commercial/government environments. Valid values include "commercial", "gcc", "gcchigh", or "dod". - For M365 tenants with E3/E5 licenses enter the value **"commercial"**. - For M365 Government Commercial Cloud tenants with G3/G5 licenses enter the value **"gcc"**. - For M365 Government Commercial Cloud High tenants enter the value **"gcchigh"**. - For M365 Department of Defense tenants enter the value **"dod"**. Default value is 'commercial'. .Parameter OPAPath The folder location of the OPA Rego executable file. The OPA Rego executable embedded with this project is located in the project's root folder. If you want to execute the tool using a version of OPA Rego located in another folder, then customize the variable value with the full path to the alternative OPA Rego exe file. .Parameter LogIn A `$true` or `$false` variable that if set to `$true` will prompt you to provide credentials if you want to establish a connection to the specified M365 products in the **$ProductNames** variable. For most use cases, leave this variable to be `$true`. A connection is established in the current PowerShell terminal session with the first authentication. If you want to run another verification in the same PowerShell session simply set this variable to be `$false` to bypass the reauthenticating in the same session. Default is $true. Note: defender will ask for authentication even if this variable is set to `$false` .Parameter Version Will output the current ScubaGear version to the terminal without running this cmdlet. .Parameter AppID The application ID of the service principal that's used during certificate based authentication. A valid value is the GUID of the application ID (service principal). .Parameter CertificateThumbprint The thumbprint value specifies the certificate that's used for certificate base authentication. The underlying PowerShell modules retrieve the certificate from the user's certificate store. As such, a copy of the certificate must be located there. .Parameter Organization Specify the organization that's used in certificate based authentication. Use the tenant's tenantname.onmicrosoft.com domain for the parameter value. .Parameter OutPath The folder path where both the output JSON and the HTML report will be created. The folder will be created if it does not exist. Defaults to current directory. .Parameter OutFolderName The name of the folder in OutPath where both the output JSON and the HTML report will be created. Defaults to "M365BaselineConformance". The client's local timestamp will be appended. .Parameter OutProviderFileName The name of the Provider output JSON created in the folder created in OutPath. Defaults to "ProviderSettingsExport". .Parameter OutRegoFileName The name of the Rego output JSON and CSV created in the folder created in OutPath. Defaults to "TestResults". .Parameter OutReportName The name of the main html file page created in the folder created in OutPath. Defaults to "BaselineReports". .Parameter MergeJson Set switch to merge all json output into a single file and delete the individual files after merging. .Parameter OutJsonFileName If MergeJson is set, the name of the consolidated json created in the folder created in OutPath. Defaults to "ScubaResults". .Parameter DisconnectOnExit Set switch to disconnect all active connections on exit from ScubaGear (default: $false) .Parameter ConfigFilePath Local file path to a JSON or YAML formatted configuration file. Configuration file parameters can be used in place of command-line parameters. Additional parameters and variables not available on the command line can also be included in the file that will be provided to the tool for use in specific tests. .Parameter DarkMode Set switch to enable report dark mode by default. .Parameter Quiet Do not launch external browser for report. .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_*. .Example Invoke-SCuBA -Version This example returns the version of SCuBAGear. .Example Invoke-SCuBA -ConfigFilePath MyConfig.json This example uses the specified configuration file when executing SCuBAGear. .Example Invoke-SCuBA -ProductNames aad, defender -OPAPath . -OutPath . The example will run the tool against the Azure Active Directory, and Defender security baselines. .Example Invoke-SCuBA -ProductNames * -M365Environment dod -OPAPath . -OutPath . This example will run the tool against all available security baselines with the 'dod' teams endpoint. .Example Invoke-SCuBA -ProductNames aad,exo -M365Environment gcc -OPAPath . -OutPath . -DisconnectOnExit Run the tool against Azure Active Directory and Exchange Online security baselines, disconnecting connections for those products when complete. .Example Invoke-SCuBA -ProductNames * -CertificateThumbprint <insert-thumbprint> -AppID <insert-appid> -Organization "tenant.onmicrosoft.com" This example will run the tool against all available security baselines while authenticating using a Service Principal with the CertificateThumprint bundle of parameters. .Functionality Public #> [CmdletBinding(DefaultParameterSetName='Report')] param ( [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')] [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", '*', IgnoreCase = $false)] [string[]] $ProductNames = [ScubaConfig]::ScubaDefault('DefaultProductNames'), [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')] [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateSet("commercial", "gcc", "gcchigh", "dod", IgnoreCase = $false)] [ValidateNotNullOrEmpty()] [string] $M365Environment = [ScubaConfig]::ScubaDefault('DefaultM365Environment'), [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')] [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateScript({Test-Path -PathType Container $_})] [string] $OPAPath = [ScubaConfig]::ScubaDefault('DefaultOPAPath'), [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')] [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [ValidateSet($true, $false)] [boolean] $LogIn = [ScubaConfig]::ScubaDefault('DefaultLogIn'), [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')] [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [switch] $DisconnectOnExit, [Parameter(ParameterSetName = 'VersionOnly')] [ValidateNotNullOrEmpty()] [switch] $Version, [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')] [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [string] $AppID, [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')] [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [string] $CertificateThumbprint, [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')] [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [string] $Organization, [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')] [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [string] $OutPath = [ScubaConfig]::ScubaDefault('DefaultOutPath'), [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')] [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [string] $OutFolderName = [ScubaConfig]::ScubaDefault('DefaultOutFolderName'), [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')] [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [string] $OutProviderFileName = [ScubaConfig]::ScubaDefault('DefaultOutProviderFileName'), [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')] [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [string] $OutRegoFileName = [ScubaConfig]::ScubaDefault('DefaultOutRegoFileName'), [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')] [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [string] $OutReportName = [ScubaConfig]::ScubaDefault('DefaultOutReportName'), [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')] [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [switch] $MergeJson, [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')] [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [string] $OutJsonFileName = [ScubaConfig]::ScubaDefault('DefaultOutJsonFileName'), [Parameter(Mandatory = $true, ParameterSetName = 'Configuration')] [ValidateNotNullOrEmpty()] [ValidateScript({ if (-Not ($_ | Test-Path)){ throw "SCuBA configuration file or folder does not exist. $_" } if (-Not ($_ | Test-Path -PathType Leaf)){ throw "SCuBA configuration Path argument must be a file." } return $true })] [System.IO.FileInfo] $ConfigFilePath, [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')] [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [switch] $DarkMode, [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')] [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [switch] $Quiet ) process { # Retrive 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 if ($Version) { Write-Output("SCuBA Gear v$ModuleVersion") return } # Transform ProductNames into list of all products if it contains wildcard if ($ProductNames.Contains('*')){ $ProductNames = $PSBoundParameters['ProductNames'] = "aad", "defender", "exo", "powerplatform", "sharepoint", "teams" Write-Debug "Setting ProductName to all products because of wildcard" } # Default execution ParameterSet if ($PSCmdlet.ParameterSetName -eq 'Report'){ $ProvidedParameters = @{ 'ProductNames' = $ProductNames | Sort-Object -Unique 'M365Environment' = $M365Environment 'OPAPath' = $OPAPath 'LogIn' = $LogIn 'DisconnectOnExit' = $DisconnectOnExit 'OutPath' = $OutPath 'OutFolderName' = $OutFolderName 'OutProviderFileName' = $OutProviderFileName 'OutRegoFileName' = $OutRegoFileName 'OutReportName' = $OutReportName 'MergeJson' = $MergeJson 'OutJsonFileName' = $OutJsonFileName } $ScubaConfig = New-Object -Type PSObject -Property $ProvidedParameters } Remove-Resources # Unload helper modules if they are still in the PowerShell session Import-Resources # Imports Providers, RunRego, CreateReport, Connection # Loads and executes parameters from a Configuration file if ($PSCmdlet.ParameterSetName -eq 'Configuration'){ [ScubaConfig]::ResetInstance() if (-Not ([ScubaConfig]::GetInstance().LoadConfig($ConfigFilePath))){ Write-Error -Message "The config file failed to load: $ConfigFilePath" } else { $ScubaConfig = [ScubaConfig]::GetInstance().Configuration } # Authentications parameters use below $SPparams = 'AppID', 'CertificateThumbprint', 'Organization' # Bound parameters indicate a parameter has been passed in. # However authentication parameters are special and are not handled within # the config module (since you can't make a default). If an authentication # parameter is set in the config file but not supplied on the command line # set the Bound parameters value which make it appear as if it was supplied on the # command line foreach ( $value in $SPparams ) { if ( $ScubaConfig[$value] -and (-not $PSBoundParameters[$value] )) { $PSBoundParameters.Add($value, $ScubaConfig[$value]) } } # Now the bound parameters contain the following # 1) Non Authentication Parameters explicitly passed in # 2) Authentication parameters ( passed in or from the config file as per code above ) # # So to provide for a command line override of config values just set the corresponding # config value from the bound parameters to override. This is redundant copy for # the authentication parameters ( but keeps the logic simpler) # We do not allow ConfigFilePath to be copied as it will be propagated to the # config module by reference and causes issues # foreach ( $value in $PSBoundParameters.keys ) { if ( $value -ne "ConfigFilePath" ) { $ScubaConfig[$value] = $PSBoundParameters[$value] } } } # Creates the output folder $Date = Get-Date -ErrorAction 'Stop' $FormattedTimeStamp = $Date.ToString("yyyy_MM_dd_HH_mm_ss") $OutFolderPath = $ScubaConfig.OutPath $FolderName = "$($ScubaConfig.OutFolderName)_$($FormattedTimeStamp)" New-Item -Path $OutFolderPath -Name $($FolderName) -ItemType Directory -ErrorAction 'Stop' | Out-Null $OutFolderPath = Join-Path -Path $OutFolderPath -ChildPath $FolderName -ErrorAction 'Stop' # Product Authentication $ConnectionParams = @{ 'LogIn' = $ScubaConfig.LogIn; 'ProductNames' = $ScubaConfig.ProductNames; 'M365Environment' = $ScubaConfig.M365Environment; 'BoundParameters' = $PSBoundParameters; } $ProdAuthFailed = Invoke-Connection @ConnectionParams if ($ProdAuthFailed.Count -gt 0) { $ScubaConfig.ProductNames = Compare-ProductList -ProductNames $ScubaConfig.ProductNames ` -ProductsFailed $ProdAuthFailed ` -ExceptionMessage 'All indicated Products were unable to authenticate' } # Tenant Metadata for the Report $TenantDetails = Get-TenantDetail -ProductNames $ScubaConfig.ProductNames -M365Environment $ScubaConfig.M365Environment try { # Provider Execution $ProviderParams = @{ 'ProductNames' = $ScubaConfig.ProductNames; 'M365Environment' = $ScubaConfig.M365Environment; 'TenantDetails' = $TenantDetails; 'ModuleVersion' = $ModuleVersion; 'OutFolderPath' = $OutFolderPath; 'OutProviderFileName' = $ScubaConfig.OutProviderFileName; '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' } # OPA Rego invocation $RegoParams = @{ 'ProductNames' = $ScubaConfig.ProductNames; 'OPAPath' = $ScubaConfig.OPAPath; 'ParentPath' = $ParentPath; 'OutFolderPath' = $OutFolderPath; 'OutProviderFileName' = $ScubaConfig.OutProviderFileName; 'OutRegoFileName' = $ScubaConfig.OutRegoFileName; } $ProdRegoFailed = Invoke-RunRego @RegoParams if ($ProdRegoFailed.Count -gt 0) { $ScubaConfig.ProductNames = Compare-ProductList -ProductNames $ScubaConfig.ProductNames ` -ProductsFailed $ProdRegoFailed ` -ExceptionMessage 'All indicated Product Rego invocations failed' } # Report Creation # Converted back from JSON String for PS Object use $TenantDetails = $TenantDetails | ConvertFrom-Json $ReportParams = @{ 'ProductNames' = $ScubaConfig.ProductNames 'TenantDetails' = $TenantDetails 'ModuleVersion' = $ModuleVersion 'OutFolderPath' = $OutFolderPath 'OutProviderFileName' = $ScubaConfig.OutProviderFileName 'OutRegoFileName' = $ScubaConfig.OutRegoFileName 'OutReportName' = $ScubaConfig.OutReportName 'DarkMode' = $DarkMode 'Quiet' = $Quiet } Invoke-ReportCreation @ReportParams if ($MergeJson) { # Craft the complete json version of the output $JsonParams = @{ 'ProductNames' = $ScubaConfig.ProductNames; 'OutFolderPath' = $OutFolderPath; 'OutProviderFileName' = $ScubaConfig.OutProviderFileName; 'TenantDetails' = $TenantDetails; 'ModuleVersion' = $ModuleVersion; 'OutJsonFileName' = $ScubaConfig.OutJsonFileName; } Merge-JsonOutput @JsonParams } } finally { if ($ScubaConfig.DisconnectOnExit) { if ($VerbosePreference -eq "Continue") { Disconnect-SCuBATenant -ProductNames $ScubaConfig.ProductNames -ErrorAction SilentlyContinue -Verbose } else { Disconnect-SCuBATenant -ProductNames $ScubaConfig.ProductNames -ErrorAction SilentlyContinue } } [ScubaConfig]::ResetInstance() } } } $ArgToProd = @{ teams = "Teams"; exo = "EXO"; defender = "Defender"; aad = "AAD"; powerplatform = "PowerPlatform"; sharepoint = "SharePoint"; } $ProdToFullName = @{ Teams = "Microsoft Teams"; EXO = "Exchange Online"; Defender = "Microsoft 365 Defender"; AAD = "Azure Active Directory"; PowerPlatform = "Microsoft Power Platform"; SharePoint = "SharePoint Online"; } $IndividualReportFolderName = "IndividualReports" function Get-FileEncoding{ <# .Description This function returns encoding type for setting content. .Functionality Internal #> $PSVersion = $PSVersionTable.PSVersion $Encoding = 'utf8' if ($PSVersion -ge '6.0'){ $Encoding = 'utf8NoBom' } return $Encoding } function Invoke-ProviderList { <# .Description This function runs the various providers modules stored in the Providers Folder Output will be stored as a ProviderSettingsExport.json in the OutPath Folder .Functionality Internal #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", '*', IgnoreCase = $false)] [string[]] $ProductNames, [Parameter(Mandatory = $true)] [ValidateSet("commercial", "gcc", "gcchigh", "dod", IgnoreCase = $false)] [ValidateNotNullOrEmpty()] [string] $M365Environment, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $TenantDetails, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $ModuleVersion, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $OutFolderPath, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $OutProviderFileName, [Parameter(Mandatory = $true)] [hashtable] $BoundParameters ) process { try { # yes the syntax has to be like this # fixing the spacing causes PowerShell interpreter errors $ProviderJSON = @" "@ $N = 0 $Len = $ProductNames.Length $ProdProviderFailed = @() $ConnectTenantParams = @{ 'M365Environment' = $M365Environment } $SPOProviderParams = @{ 'M365Environment' = $M365Environment } $PnPFlag = $false if ($BoundParameters.AppID) { $ServicePrincipalParams = Get-ServicePrincipalParams -BoundParameters $BoundParameters $ConnectTenantParams += @{ServicePrincipalParams = $ServicePrincipalParams; } $PnPFlag = $true $SPOProviderParams += @{PnPFlag = $PnPFlag } } foreach ($Product in $ProductNames) { $BaselineName = $ArgToProd[$Product] $N += 1 $Percent = $N * 100 / $Len $Status = "Running the $($BaselineName) Provider; $($N) of $($Len) Product settings extracted" $ProgressParams = @{ 'Activity' = "Running the provider for each baseline"; 'Status' = $Status; 'PercentComplete' = $Percent; 'Id' = 1; 'ErrorAction' = 'Stop'; } Write-Progress @ProgressParams try { $RetVal = "" switch ($Product) { "aad" { $RetVal = Export-AADProvider | Select-Object -Last 1 } "exo" { $RetVal = Export-EXOProvider | Select-Object -Last 1 } "defender" { $RetVal = Export-DefenderProvider @ConnectTenantParams | Select-Object -Last 1 } "powerplatform" { $RetVal = Export-PowerPlatformProvider -M365Environment $M365Environment | Select-Object -Last 1 } "sharepoint" { $RetVal = Export-SharePointProvider @SPOProviderParams | Select-Object -Last 1 } "teams" { $RetVal = Export-TeamsProvider | Select-Object -Last 1 } default { Write-Error -Message "Invalid ProductName argument" } } $ProviderJSON += $RetVal } catch { Write-Error "Error with the $($BaselineName) Provider. See the exception message for more details: $($_)" $ProdProviderFailed += $Product Write-Warning "$($Product) will be omitted from the output because of the failure above `n`n" } } $ProviderJSON = $ProviderJSON.TrimEnd(",") $TimeZone = "" $CurrentDate = Get-Date -ErrorAction 'Stop' $TimestampZulu = $CurrentDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ") $GetTimeZone = Get-TimeZone -ErrorAction 'Stop' if (($CurrentDate).IsDaylightSavingTime()) { $TimeZone = ($GetTimeZone).DaylightName } else { $TimeZone = ($GetTimeZone).StandardName } $ConfigDetails = @(ConvertTo-Json -Depth 100 $([ScubaConfig]::GetInstance().Configuration)) if(! $ConfigDetails) { $ConfigDetails = "{}" } $BaselineSettingsExport = @" { "baseline_version": "1", "module_version": "$ModuleVersion", "date": "$($CurrentDate) $($TimeZone)", "timestamp_zulu": "$($TimestampZulu)", "tenant_details": $($TenantDetails), "scuba_config": $($ConfigDetails), $ProviderJSON } "@ # Strip the character sequences that Rego tries to interpret as escape sequences, # resulting in the error "unable to parse input: yaml: line x: found unknown escape character" # "\/", ex: "\/Date(1705651200000)\/" $BaselineSettingsExport = $BaselineSettingsExport.replace("\/", "") # "\B", ex: "Removed an entry in Tenant Allow\Block List" $BaselineSettingsExport = $BaselineSettingsExport.replace("\B", "/B") $FinalPath = Join-Path -Path $OutFolderPath -ChildPath "$($OutProviderFileName).json" -ErrorAction 'Stop' $BaselineSettingsExport | Set-Content -Path $FinalPath -Encoding $(Get-FileEncoding) -ErrorAction 'Stop' $ProdProviderFailed } catch { $InvokeProviderListErrorMessage = "Fatal Error involving the Provider functions. ` Ending ScubaGear execution. See the exception message for more details: $($_)" throw $InvokeProviderListErrorMessage } } } function Invoke-RunRego { <# .Description This function runs the RunRego module. Which runs the various rego files against the ProviderSettings.json using the specified OPA executable Output will be stored as a TestResults.json in the OutPath Folder .Functionality Internal #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", '*', IgnoreCase = $false)] [string[]] $ProductNames, [ValidateNotNullOrEmpty()] [string] $OPAPath = [ScubaConfig]::ScubaDefault('DefaultOPAPath'), [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $ParentPath, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $OutFolderPath, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $OutProviderFileName, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $OutRegoFileName ) process { try { $ProdRegoFailed = @() $TestResults = @() $N = 0 $Len = $ProductNames.Length foreach ($Product in $ProductNames) { $BaselineName = $ArgToProd[$Product] $N += 1 $Percent = $N * 100 / $Len $Status = "Running the $($BaselineName) Rego Verification; $($N) of $($Len) Rego verifications completed" $ProgressParams = @{ 'Activity' = "Running the rego for each baseline"; 'Status' = $Status; 'PercentComplete' = $Percent; 'Id' = 1; 'ErrorAction' = 'Stop'; } Write-Progress @ProgressParams $InputFile = Join-Path -Path $OutFolderPath "$($OutProviderFileName).json" -ErrorAction 'Stop' $RegoFile = Join-Path -Path $ParentPath -ChildPath "Rego" -ErrorAction 'Stop' $RegoFile = Join-Path -Path $RegoFile -ChildPath "$($BaselineName)Config.rego" -ErrorAction 'Stop' $params = @{ 'InputFile' = $InputFile; 'RegoFile' = $RegoFile; 'PackageName' = $Product; 'OPAPath' = $OPAPath } try { $RetVal = Invoke-Rego @params $TestResults += $RetVal } catch { Write-Error "Error with the $($BaselineName) Rego invocation. See the exception message for more details: $($_)" $ProdRegoFailed += $Product Write-Warning "$($Product) will be omitted from the output because of the failure above" } } $TestResultsJson = $TestResults | ConvertTo-Json -Depth 5 -ErrorAction 'Stop' $FileName = Join-Path -Path $OutFolderPath "$($OutRegoFileName).json" -ErrorAction 'Stop' $TestResultsJson | Set-Content -Path $FileName -Encoding $(Get-FileEncoding) -ErrorAction 'Stop' foreach ($Product in $TestResults) { foreach ($Test in $Product) { # ConvertTo-Csv struggles with the nested nature of the ActualValue # and Commandlet fields. Explicitly convert these to json strings before # calling ConvertTo-Csv $Test.ActualValue = $Test.ActualValue | ConvertTo-Json -Depth 3 -Compress -ErrorAction 'Stop' $Test.Commandlet = $Test.Commandlet -Join ", " } } $TestResultsCsv = $TestResults | ConvertTo-Csv -NoTypeInformation -ErrorAction 'Stop' $CSVFileName = Join-Path -Path $OutFolderPath "$($OutRegoFileName).csv" -ErrorAction 'Stop' $TestResultsCsv | Set-Content -Path $CSVFileName -Encoding $(Get-FileEncoding) -ErrorAction 'Stop' $ProdRegoFailed } catch { $InvokeRegoErrorMessage = "Fatal Error involving the OPA output function. ` Ending ScubaGear execution. See the exception message for more details: $($_)" throw $InvokeRegoErrorMessage } } } function Pluralize { <# .Description This function whether the singular or plural version of the noun is needed and returns the appropriate version. .Functionality Internal #> [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] $SingularNoun, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] $PluralNoun, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [int] $Count ) process { if ($Count -gt 1) { $PluralNoun } else { $SingularNoun } } } function Merge-JsonOutput { <# .Description This function packages all the json output created into a single json file. .Functionality Internal #> [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", '*', IgnoreCase = $false)] [string[]] $ProductNames, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] $OutFolderPath, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] $OutProviderFileName, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [object] $TenantDetails, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] $ModuleVersion, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] $OutJsonFileName ) process { try { # Files to delete at the end if no errors are encountered $DeletionList = @() # Load the raw provider output $SettingsExportPath = Join-Path $OutFolderPath -ChildPath "$($OutProviderFileName).json" $DeletionList += $SettingsExportPath $SettingsExport = Get-Content $SettingsExportPath -Raw $TimestampZulu = $(ConvertFrom-Json $SettingsExport).timestamp_zulu # Get a list and abbreviation mapping of the products assessed $FullNames = @() $ProductAbbreviationMapping = @{} foreach ($ProductName in $ProductNames) { $BaselineName = $ArgToProd[$ProductName] $FullNames += $ProdToFullName[$BaselineName] $ProductAbbreviationMapping[$ProdToFullName[$BaselineName]] = $BaselineName } # Extract the metadata $Results = [pscustomobject]@{} $Summary = [pscustomobject]@{} $MetaData = [pscustomobject]@{ "TenantId" = $TenantDetails.TenantId; "DisplayName" = $TenantDetails.DisplayName; "DomainName" = $TenantDetails.DomainName; "ProductSuite" = "Microsoft 365"; "ProductsAssessed" = $FullNames; "ProductAbbreviationMapping" = $ProductAbbreviationMapping "Tool" = "ScubaGear"; "ToolVersion" = $ModuleVersion; "TimestampZulu" = $TimestampZulu; } # Aggregate the report results and summaries $IndividualReportPath = Join-Path -Path $OutFolderPath $IndividualReportFolderName -ErrorAction 'Stop' foreach ($Product in $ProductNames) { $BaselineName = $ArgToProd[$Product] $FileName = Join-Path $IndividualReportPath "$($BaselineName)Report.json" $DeletionList += $FileName $IndividualResults = Get-Content $FileName | ConvertFrom-Json $Results | Add-Member -NotePropertyName $BaselineName ` -NotePropertyValue $IndividualResults.Results # The date is listed under the metadata, no need to include it in the summary as well $IndividualResults.ReportSummary.PSObject.Properties.Remove('Date') $Summary | Add-Member -NotePropertyName $BaselineName ` -NotePropertyValue $IndividualResults.ReportSummary } # Convert the output a json string $MetaData = ConvertTo-Json $MetaData -Depth 3 $Results = ConvertTo-Json $Results -Depth 5 $Summary = ConvertTo-Json $Summary -Depth 3 $ReportJson = @" { "MetaData": $MetaData, "Summary": $Summary, "Results": $Results, "Raw": $SettingsExport } "@ # ConvertTo-Json for some reason converts the <, >, and ' characters into unicode escape sequences. # Convert those back to ASCII. $ReportJson = $ReportJson.replace("\u003c", "<") $ReportJson = $ReportJson.replace("\u003e", ">") $ReportJson = $ReportJson.replace("\u0027", "'") # Save the file $JsonFileName = Join-Path -Path $OutFolderPath "$($OutJsonFileName).json" -ErrorAction 'Stop' $ReportJson | Set-Content -Path $JsonFileName -Encoding $(Get-FileEncoding) -ErrorAction 'Stop' # Delete the now redundant files foreach ($File in $DeletionList) { Remove-Item $File } } catch { $MergeJsonErrorMessage = "Fatal Error involving the Json reports aggregation. ` Ending ScubaGear execution. See the exception message for more details: $($_)" throw $MergeJsonErrorMessage } } } function Invoke-ReportCreation { <# .Description This function runs the CreateReport Module which creates an HTML report using the TestResults.json. Output will be stored as various HTML files in the OutPath Folder. The report Home page will be named BaselineReports.html .Functionality Internal #> [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", '*', IgnoreCase = $false)] [string[]] $ProductNames, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [object] $TenantDetails, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] $ModuleVersion, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] $OutFolderPath, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] $OutProviderFileName, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] $OutRegoFileName, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] $OutReportName, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [switch] $Quiet, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [switch] $DarkMode ) process { try { $N = 0 $Len = $ProductNames.Length $Fragment = @() $IndividualReportPath = Join-Path -Path $OutFolderPath -ChildPath $IndividualReportFolderName New-Item -Path $IndividualReportPath -ItemType "Directory" -ErrorAction "SilentlyContinue" | Out-Null $ReporterPath = Join-Path -Path $PSScriptRoot -ChildPath "CreateReport" -ErrorAction 'Stop' $Images = Join-Path -Path $ReporterPath -ChildPath "images" -ErrorAction 'Stop' Copy-Item -Path $Images -Destination $IndividualReportPath -Force -Recurse -ErrorAction 'Stop' $SecureBaselines = Import-SecureBaseline -ProductNames $ProductNames foreach ($Product in $ProductNames) { $BaselineName = $ArgToProd[$Product] $N += 1 $Percent = $N*100/$Len $Status = "Running the $($BaselineName) Report creation; $($N) of $($Len) Baselines Reports created"; $ProgressParams = @{ 'Activity' = "Creating the reports for each baseline"; 'Status' = $Status; 'PercentComplete' = $Percent; 'Id' = 1; 'ErrorAction' = 'Stop'; } Write-Progress @ProgressParams $FullName = $ProdToFullName[$BaselineName] $CreateReportParams = @{ 'BaselineName' = $BaselineName; 'FullName' = $FullName; 'IndividualReportPath' = $IndividualReportPath; 'OutPath' = $OutFolderPath; 'OutProviderFileName' = $OutProviderFileName; 'OutRegoFileName' = $OutRegoFileName; 'DarkMode' = $DarkMode; 'SecureBaselines' = $SecureBaselines } $Report = New-Report @CreateReportParams $LinkPath = "$($IndividualReportFolderName)/$($BaselineName)Report.html" $LinkClassName = '"individual_reports"' # uses no escape characters $Link = "<a class=$($LinkClassName) href='$($LinkPath)'>$($FullName)</a>" $PassesSummary = "<div class='summary pass'>$($Report.Passes) tests passed</div>" $WarningsSummary = "<div class='summary'></div>" $FailuresSummary = "<div class='summary'></div>" $BaselineURL = "<a href= `"https://github.com/cisagov/ScubaGear/blob/v$($ModuleVersion)/baselines`" target=`"_blank`"><h3 style=`"width: 100px;`">Baseline Documents</h3></a>" $ManualSummary = "<div class='summary'></div>" $ErrorSummary = "<div class='summary'></div>" if ($Report.Warnings -gt 0) { $Noun = Pluralize -SingularNoun "warning" -PluralNoun "warnings" -Count $Report.Warnings $WarningsSummary = "<div class='summary warning'>$($Report.Warnings) $($Noun)</div>" } if ($Report.Failures -gt 0) { $Noun = Pluralize -SingularNoun "test" -PluralNoun "tests" -Count $Report.Failures $FailuresSummary = "<div class='summary failure'>$($Report.Failures) $($Noun) failed</div>" } if ($Report.Manual -gt 0) { $Noun = Pluralize -SingularNoun "check" -PluralNoun "checks" -Count $Report.Manual $ManualSummary = "<div class='summary manual'>$($Report.Manual) manual $($Noun) needed</div>" } if ($Report.Errors -gt 0) { $Noun = Pluralize -SingularNoun "error" -PluralNoun "errors" -Count $Report.Errors $ErrorSummary = "<div class='summary error'>$($Report.Errors) $($Noun)</div>" } $Fragment += [pscustomobject]@{ "Baseline Conformance Reports" = $Link; "Details" = "$($PassesSummary) $($WarningsSummary) $($FailuresSummary) $($ManualSummary) $($ErrorSummary)" } } $TenantMetaData += [pscustomobject]@{ "Tenant Display Name" = $TenantDetails.DisplayName; "Tenant Domain Name" = $TenantDetails.DomainName "Tenant ID" = $TenantDetails.TenantId; "Report Date" = $Report.Date; } $TenantMetaData = $TenantMetaData | ConvertTo-Html -Fragment -ErrorAction 'Stop' $TenantMetaData = $TenantMetaData -replace '^(.*?)<table>','<table class ="tenantdata" style = "text-align:center;">' $Fragment = $Fragment | ConvertTo-Html -Fragment -ErrorAction 'Stop' $ReportHtmlPath = Join-Path -Path $ReporterPath -ChildPath "ParentReport" -ErrorAction 'Stop' $ReportHTML = (Get-Content $(Join-Path -Path $ReportHtmlPath -ChildPath "ParentReport.html") -ErrorAction 'Stop') -Join "`n" $ReportHTML = $ReportHTML.Replace("{TENANT_DETAILS}", $TenantMetaData) $ReportHTML = $ReportHTML.Replace("{TABLES}", $Fragment) $ReportHTML = $ReportHTML.Replace("{MODULE_VERSION}", "v$ModuleVersion") $ReportHTML = $ReportHTML.Replace("{BASELINE_URL}", $BaselineURL) $CssPath = Join-Path -Path $ReporterPath -ChildPath "styles" -ErrorAction 'Stop' $MainCSS = (Get-Content $(Join-Path -Path $CssPath -ChildPath "main.css") -ErrorAction 'Stop') -Join "`n" $ReportHTML = $ReportHTML.Replace("{MAIN_CSS}", "<style>$($MainCSS)</style>") $ParentCSS = (Get-Content $(Join-Path -Path $CssPath -ChildPath "ParentReportStyle.css") -ErrorAction 'Stop') -Join "`n" $ReportHTML = $ReportHTML.Replace("{PARENT_CSS}", "<style>$($ParentCSS)</style>") $ScriptsPath = Join-Path -Path $ReporterPath -ChildPath "scripts" -ErrorAction 'Stop' $ParentReportJS = (Get-Content $(Join-Path -Path $ScriptsPath -ChildPath "ParentReport.js") -ErrorAction 'Stop') -Join "`n" $UtilsJS = (Get-Content $(Join-Path -Path $ScriptsPath -ChildPath "utils.js") -ErrorAction 'Stop') -Join "`n" $ParentReportJS = "$($ParentReportJS)`n$($UtilsJS)" $ReportHTML = $ReportHTML.Replace("{MAIN_JS}", "<script> let darkMode = $($DarkMode.ToString().ToLower()); $($ParentReportJS) </script>") Add-Type -AssemblyName System.Web -ErrorAction 'Stop' $ReportFileName = Join-Path -Path $OutFolderPath "$($OutReportName).html" -ErrorAction 'Stop' [System.Web.HttpUtility]::HtmlDecode($ReportHTML) | Out-File $ReportFileName -ErrorAction 'Stop' if (-Not $Quiet) { Invoke-Item $ReportFileName } } catch { $InvokeReportErrorMessage = "Fatal Error involving the Report Creation. ` Ending ScubaGear execution. See the exception message for more details: $($_)" throw $InvokeReportErrorMessage } } } function Get-TenantDetail { <# .Description This function gets the details of the M365 Tenant using the various M365 PowerShell modules .Functionality Internal #> [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", IgnoreCase = $false)] [ValidateNotNullOrEmpty()] [string[]] $ProductNames, [Parameter(Mandatory = $true)] [ValidateSet("commercial", "gcc", "gcchigh", "dod", IgnoreCase = $false)] [ValidateNotNullOrEmpty()] [string] $M365Environment ) # organized by best tenant details information if ($ProductNames.Contains("aad")) { Get-AADTenantDetail } elseif ($ProductNames.Contains("sharepoint")) { Get-AADTenantDetail } elseif ($ProductNames.Contains("teams")) { Get-TeamsTenantDetail -M365Environment $M365Environment } elseif ($ProductNames.Contains("powerplatform")) { Get-PowerPlatformTenantDetail -M365Environment $M365Environment } elseif ($ProductNames.Contains("exo")) { Get-EXOTenantDetail -M365Environment $M365Environment } elseif ($ProductNames.Contains("defender")) { Get-EXOTenantDetail -M365Environment $M365Environment } else { $TenantInfo = @{ "DisplayName" = "Orchestrator Error retrieving Display name"; "DomainName" = "Orchestrator Error retrieving Domain name"; "TenantId" = "Orchestrator Error retrieving Tenant ID"; "AdditionalData" = "Orchestrator Error retrieving additional data"; } $TenantInfo = $TenantInfo | ConvertTo-Json -Depth 3 $TenantInfo } } function Invoke-Connection { <# .Description This function uses the Connection.psm1 module which uses the various PowerShell modules to establish a connection to an M365 Tenant associated with provided credentials .Functionality Internal #> [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [boolean] $LogIn, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", '*', IgnoreCase = $false)] [string[]] $ProductNames, [ValidateSet("commercial", "gcc", "gcchigh", "dod")] [ValidateNotNullOrEmpty()] [string] $M365Environment = "commercial", [Parameter(Mandatory=$true)] [hashtable] $BoundParameters ) $ConnectTenantParams = @{ 'ProductNames' = $ProductNames; 'M365Environment' = $M365Environment } if ($BoundParameters.AppID) { $ServicePrincipalParams = Get-ServicePrincipalParams -BoundParameters $BoundParameters $ConnectTenantParams += @{ServicePrincipalParams = $ServicePrincipalParams;} } if ($LogIn) { $AnyFailedAuth = Connect-Tenant @ConnectTenantParams $AnyFailedAuth } } function Compare-ProductList { <# .Description Compares two ProductNames Lists and returns the Diff between them Used to compare a failed execution list with the original list .Functionality Internal #> param( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", '*', IgnoreCase = $false)] [string[]] $ProductNames, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", '*', IgnoreCase = $false)] [string[]] $ProductsFailed, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] $ExceptionMessage ) $Difference = Compare-Object $ProductNames -DifferenceObject $ProductsFailed -PassThru if (-not $Difference) { throw "$($ExceptionMessage); aborting ScubaGear execution" } else { $Difference } } function Get-ServicePrincipalParams { <# .Description Returns a valid a hastable of parameters for authentication via Service Principal. Throws an error if there are none. .Functionality Internal #> [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [hashtable] $BoundParameters ) $ServicePrincipalParams = @{} $CheckThumbprintParams = ($BoundParameters.CertificateThumbprint) ` -and ($BoundParameters.AppID) -and ($BoundParameters.Organization) if ($CheckThumbprintParams) { $CertThumbprintParams = @{ CertificateThumbprint = $BoundParameters.CertificateThumbprint; AppID = $BoundParameters.AppID; Organization = $BoundParameters.Organization; } $ServicePrincipalParams += @{CertThumbprintParams = $CertThumbprintParams} } else { throw "Missing parameters required for authentication with Service Principal Auth; Run Get-Help Invoke-Scuba for details on correct arguments" } $ServicePrincipalParams } function Import-Resources { <# .Description This function imports all of the various helper Provider, Rego, and Reporter modules to the runtime .Functionality Internal #> [CmdletBinding()] param() try { $ProvidersPath = Join-Path -Path $PSScriptRoot ` -ChildPath "Providers" ` -Resolve ` -ErrorAction 'Stop' $ProviderResources = Get-ChildItem $ProvidersPath -Recurse | Where-Object { $_.Name -like 'Export*.psm1' } if (!$ProviderResources) { throw "Provider files were not found, aborting this run" } foreach ($Provider in $ProviderResources.Name) { $ProvidersPath = Join-Path -Path $PSScriptRoot -ChildPath "Providers" -ErrorAction 'Stop' $ModulePath = Join-Path -Path $ProvidersPath -ChildPath $Provider -ErrorAction 'Stop' Import-Module $ModulePath } @('Connection', 'RunRego', 'CreateReport', 'ScubaConfig', 'Support') | ForEach-Object { $ModulePath = Join-Path -Path $PSScriptRoot -ChildPath $_ -ErrorAction 'Stop' Write-Debug "Importing $_ module" Import-Module -Name $ModulePath } } catch { $ImportResourcesErrorMessage = "Fatal Error involving importing PowerShell modules. ` Ending ScubaGear execution. See the exception message for more details: $($_)" throw $ImportResourcesErrorMessage } } function Remove-Resources { <# .Description This function cleans up all of the various imported modules Mostly meant for dev work .Functionality Internal #> [CmdletBinding()] $Providers = @("ExportPowerPlatform", "ExportEXOProvider", "ExportAADProvider", "ExportDefenderProvider", "ExportTeamsProvider", "ExportSharePointProvider") foreach ($Provider in $Providers) { Remove-Module $Provider -ErrorAction "SilentlyContinue" } Remove-Module "ScubaConfig" -ErrorAction "SilentlyContinue" Remove-Module "RunRego" -ErrorAction "SilentlyContinue" Remove-Module "CreateReport" -ErrorAction "SilentlyContinue" Remove-Module "Connection" -ErrorAction "SilentlyContinue" } function Invoke-RunCached { <# .SYNOPSIS Specially execute the SCuBAGear tool security baselines for specified M365 products. Can be executed on static provider JSON. .Description This is the function for running the tool provider JSON that has already been extracted. This functions comes with the extra ExportProvider parameter to omit exporting the provider if set to $false. The rego will be run on a static provider JSON in the specified OutPath. <# .Parameter ExportProvider This parameter will when set to $true export the provider and act like Invoke-Scuba. When set to $false will instead omit authentication plus pulling the provider and will instead look in OutPath and run just the Rego verification and Report creation. .Parameter ProductNames A list of one or more M365 shortened product names that the tool will assess when it is executed. Acceptable product name values are listed below. To assess Azure Active Directory you would enter the value aad. To assess Exchange Online you would enter exo and so forth. - Azure Active Directory: aad - Defender for Office 365: defender - Exchange Online: exo - MS Power Platform: powerplatform - SharePoint Online: sharepoint - MS Teams: teams. Use '*' to run all baselines. .Parameter M365Environment This parameter is used to authenticate to the different commercial/government environments. Valid values include "commercial", "gcc", "gcchigh", or "dod". For M365 tenants with E3/E5 licenses enter the value **"commercial"**. For M365 Government Commercial Cloud tenants with G3/G5 licenses enter the value **"gcc"**. For M365 Government Commercial Cloud High tenants enter the value **"gcchigh"**. For M365 Department of Defense tenants enter the value **"dod"**. Default is 'commercial'. .Parameter OPAPath The folder location of the OPA Rego executable file. The OPA Rego executable embedded with this project is located in the project's root folder. If you want to execute the tool using a version of OPA Rego located in another folder, then customize the variable value with the full path to the alternative OPA Rego exe file. .Parameter LogIn A `$true` or `$false` variable that if set to `$true` will prompt you to provide credentials if you want to establish a connection to the specified M365 products in the **$ProductNames** variable. For most use cases, leave this variable to be `$true`. A connection is established in the current PowerShell terminal session with the first authentication. If you want to run another verification in the same PowerShell session simply set this variable to be `$false` to bypass the reauthenticating in the same session. Default is $true. .Parameter Version Will output the current ScubaGear version to the terminal without running this cmdlet. .Parameter AppID The application ID of the service principal that's used during certificate based authentication. A valid value is the GUID of the application ID (service principal). .Parameter CertificateThumbprint The thumbprint value specifies the certificate that's used for certificate base authentication. The underlying PowerShell modules retrieve the certificate from the user's certificate store. As such, a copy of the certificate must be located there. .Parameter Organization Specify the organization that's used in certificate based authentication. Use the tenant's tenantname.onmicrosoft.com domain for the parameter value. The folder path where both the output JSON and the HTML report will be created. The folder will be created if it does not exist. Defaults to current directory. .Parameter OutFolderName The name of the folder in OutPath where both the output JSON and the HTML report will be created. Defaults to "M365BaselineConformance". The client's local timestamp will be appended. .Parameter OutProviderFileName The name of the Provider output JSON created in the folder created in OutPath. Defaults to "ProviderSettingsExport". .Parameter OutRegoFileName The name of the Rego output JSON and CSV created in the folder created in OutPath. Defaults to "TestResults". .Parameter OutReportName The name of the main html file page created in the folder created in OutPath. Defaults to "BaselineReports". .Parameter MergeJson Set switch to merge all json output into a single file and delete the individual files after merging. .Parameter OutJsonFileName If MergeJson is set, the name of the consolidated json created in the folder created in OutPath. Defaults to "ScubaResults". .Parameter DarkMode Set switch to enable report dark mode by default. .Example Invoke-RunCached Run an assessment against by default a commercial M365 Tenant against the Azure Active Directory, Exchange Online, Microsoft Defender, One Drive, SharePoint Online, and Microsoft Teams security baselines. The output will stored in the current directory in a folder called M365BaselineConformaance_*. .Example Invoke-RunCached -Version This example returns the version of SCuBAGear. .Example Invoke-RunCached -ProductNames aad, defender -OPAPath . -OutPath . The example will run the tool against the Azure Active Directory, and Defender security baselines. .Example Invoke-RunCached -ProductNames * -M365Environment dod -OPAPath . -OutPath . This example will run the tool against all available security baselines with the 'dod' teams endpoint. .Example Invoke-SCuBA -ProductNames * -CertificateThumbprint <insert-thumbprint> -AppID <insert-appid> -Organization "tenant.onmicrosoft.com" This example will run the tool against all available security baselines while authenticating using a Service Principal with the CertificateThumprint bundle of parameters. .Functionality Public #> [CmdletBinding()] param ( [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [boolean] $ExportProvider = $true, [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", '*', IgnoreCase = $false)] [string[]] $ProductNames = [ScubaConfig]::ScubaDefault('AllProductNames'), [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateSet("commercial", "gcc", "gcchigh", "dod")] [ValidateNotNullOrEmpty()] [string] $M365Environment = [ScubaConfig]::ScubaDefault('DefaultM365Environment'), [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateScript({Test-Path -PathType Container $_})] [ValidateNotNullOrEmpty()] [string] $OPAPath = [ScubaConfig]::ScubaDefault('DefaultOPAPath'), [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [ValidateSet($true, $false)] [boolean] $LogIn = [ScubaConfig]::ScubaDefault('DefaultLogIn'), [Parameter(ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [switch] $Version, [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [string] $AppID, [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [string] $CertificateThumbprint, [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [string] $Organization, [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [string] $OutPath = [ScubaConfig]::ScubaDefault('DefaultOutPath'), [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [string] $OutProviderFileName = [ScubaConfig]::ScubaDefault('DefaultOutProviderFileName'), [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [string] $OutRegoFileName = [ScubaConfig]::ScubaDefault('DefaultOutRegoFileName'), [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [string] $OutReportName = [ScubaConfig]::ScubaDefault('DefaultOutReportName'), [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [switch] $MergeJson, [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [string] $OutJsonFileName = [ScubaConfig]::ScubaDefault('DefaultOutJsonFileName'), [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [ValidateNotNullOrEmpty()] [ValidateSet($true, $false)] [switch] $Quiet, [Parameter(Mandatory = $false, ParameterSetName = 'Report')] [switch] $DarkMode ) process { $ParentPath = Split-Path $PSScriptRoot -Parent $ScubaManifest = Import-PowerShellDataFile (Join-Path -Path $ParentPath -ChildPath 'ScubaGear.psd1' -Resolve) $ModuleVersion = $ScubaManifest.ModuleVersion if ($Version) { Write-Output("SCuBA Gear v$ModuleVersion") return } if ($ProductNames -eq '*'){ $ProductNames = "teams", "exo", "defender", "aad", "sharepoint", "powerplatform" } # Create outpath if $Outpath does not exist if(-not (Test-Path -PathType "container" $OutPath)) { New-Item -ItemType "Directory" -Path $OutPath | Out-Null } $OutFolderPath = $OutPath $ProductNames = $ProductNames | Sort-Object -Unique Remove-Resources Import-Resources # Imports Providers, RunRego, CreateReport, Connection # Authenticate $ConnectionParams = @{ 'LogIn' = $LogIn; 'ProductNames' = $ProductNames; 'M365Environment' = $M365Environment; 'BoundParameters' = $PSBoundParameters; } # Rego Testing failsafe $TenantDetails = @{"DisplayName"="Rego Testing";} $TenantDetails = $TenantDetails | ConvertTo-Json -Depth 3 if ($ExportProvider) { $ProdAuthFailed = Invoke-Connection @ConnectionParams if ($ProdAuthFailed.Count -gt 0) { $Difference = Compare-Object $ProductNames -DifferenceObject $ProdAuthFailed -PassThru if (-not $Difference) { throw "All products were unable to establish a connection aborting execution" } else { $ProductNames = $Difference } } $TenantDetails = Get-TenantDetail -ProductNames $ProductNames -M365Environment $M365Environment $ProviderParams = @{ 'ProductNames' = $ProductNames; 'M365Environment' = $M365Environment; 'TenantDetails' = $TenantDetails; 'ModuleVersion' = $ModuleVersion; 'OutFolderPath' = $OutFolderPath; 'OutProviderFileName' = $OutProviderFileName; 'BoundParameters' = $PSBoundParameters; } Invoke-ProviderList @ProviderParams } $FileName = Join-Path -Path $OutPath -ChildPath "$($OutProviderFileName).json" $SettingsExport = Get-Content $FileName | ConvertFrom-Json $TenantDetails = $SettingsExport.tenant_details $RegoParams = @{ 'ProductNames' = $ProductNames; 'OPAPath' = $OPAPath; 'ParentPath' = $ParentPath; 'OutFolderPath' = $OutFolderPath; 'OutProviderFileName' = $OutProviderFileName; 'OutRegoFileName' = $OutRegoFileName; } $ReportParams = @{ 'ProductNames' = $ProductNames; 'TenantDetails' = $TenantDetails; 'ModuleVersion' = $ModuleVersion; 'OutFolderPath' = $OutFolderPath; 'OutProviderFileName' = $OutProviderFileName; 'OutRegoFileName' = $OutRegoFileName; 'OutReportName' = $OutReportName; 'Quiet' = $Quiet; 'DarkMode' = $DarkMode; } Invoke-RunRego @RegoParams Invoke-ReportCreation @ReportParams if ($MergeJson) { # Craft the complete json version of the output $JsonParams = @{ 'ProductNames' = $ProductNames; 'OutFolderPath' = $OutFolderPath; 'OutProviderFileName' = $OutProviderFileName; 'TenantDetails' = $TenantDetails; 'ModuleVersion' = $ModuleVersion; 'OutJsonFileName' = $OutJsonFileName; } Merge-JsonOutput @JsonParams } } } Export-ModuleMember -Function @( 'Invoke-SCuBA', 'Invoke-RunCached' ) # SIG # Begin signature block # MIIuwAYJKoZIhvcNAQcCoIIusTCCLq0CAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAmuFGKFtrFXFdr # idKQRf3DsnmJA1bEHKbEwIQXU4Z5A6CCE6MwggWQMIIDeKADAgECAhAFmxtXno4h # MuI5B72nd3VcMA0GCSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNV # BAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0z # ODAxMTUxMjAwMDBaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ # bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0 # IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB # AL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/z # G6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZ # anMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7s # Wxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL # 2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfb # BHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3 # JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3c # AORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqx # YxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0 # viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aL # T8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1Ud # EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzf # Lmc/57qYrhwPTzANBgkqhkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNk # aA9Wz3eucPn9mkqZucl4XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjS # PMFDQK4dUPVS/JA7u5iZaWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK # 7VB6fWIhCoDIc2bRoAVgX+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eB # cg3AFDLvMFkuruBx8lbkapdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp # 5aPNoiBB19GcZNnqJqGLFNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msg # dDDS4Dk0EIUhFQEI6FUy3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vri # RbgjU2wGb2dVf0a1TD9uKFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ7 # 9ARj6e/CVABRoIoqyc54zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5 # nLGbsQAe79APT0JsyQq87kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3 # i0objwG2J5VT6LaJbVu8aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0H # EEcRrYc9B9F1vM/zZn4wggawMIIEmKADAgECAhAIrUCyYNKcTJ9ezam9k67ZMA0G # CSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ # bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0 # IFRydXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0zNjA0MjgyMzU5NTla # MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE # AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz # ODQgMjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDVtC9C # 0CiteLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0JAfhS0/TeEP0F9ce # 2vnS1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJrQ5qZ8sU7H/Lvy0da # E6ZMswEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhFLqGfLOEYwhrMxe6T # SXBCMo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+FLEikVoQ11vkunKoA # FdE3/hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh3K3kGKDYwSNHR7Oh # D26jq22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJwZPt4bRc4G/rJvmM # 1bL5OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQayg9Rc9hUZTO1i4F4z # 8ujo7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbIYViY9XwCFjyDKK05 # huzUtw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchApQfDVxW0mdmgRQRNY # mtwmKwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRroOBl8ZhzNeDhFMJlP # /2NPTLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IBWTCCAVUwEgYDVR0T # AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHwYD # VR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMG # A1UdJQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYY # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2Fj # ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNV # HR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRU # cnVzdGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAEDMAgGBmeBDAEEATAN # BgkqhkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql+Eg08yy25nRm95Ry # sQDKr2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFFUP2cvbaF4HZ+N3HL # IvdaqpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1hmYFW9snjdufE5Btf # Q/g+lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3RywYFzzDaju4ImhvTnh # OE7abrs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5UbdldAhQfQDN8A+KVssIh # dXNSy0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw8MzK7/0pNVwfiThV # 9zeKiwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnPLqR0kq3bPKSchh/j # wVYbKyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatEQOON8BUozu3xGFYH # Ki8QxAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bnKD+sEq6lLyJsQfmC # XBVmzGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQjiWQ1tygVQK+pKHJ6l # /aCnHwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbqyK+p/pQd52MbOoZW # eE4wggdXMIIFP6ADAgECAhANkQ8dPvvR0q3Ytt4H0T3aMA0GCSqGSIb3DQEBCwUA # MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE # AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz # ODQgMjAyMSBDQTEwHhcNMjQwMTMwMDAwMDAwWhcNMjUwMTI5MjM1OTU5WjBfMQsw # CQYDVQQGEwJVUzEdMBsGA1UECBMURGlzdHJpY3Qgb2YgQ29sdW1iaWExEzARBgNV # BAcTCldhc2hpbmd0b24xDTALBgNVBAoTBENJU0ExDTALBgNVBAMTBENJU0EwggIi # MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCT1y7uJCQax8JfiDEYgpiU9URj # EXCTRqtZbDALM9rPUudiuM3mj6A1SUSAAWYv6DTsvGPvxyMI2Idg0mQunl4Ms9DJ # yVwe5k4+Anj/73Nx1AbOPYP8xRZcD10FkctKGhV0PzvrDcwU15hsQWtiepFgg+bX # fHkGMeu426oc69f43vKE43DiqKTf0/UBX/qgpj3JZvJ3zc1kilBOv4sBCksfCjbW # tLZD0tqAgBsNPo3Oy5mQG31E1eZdTNvrdTnEXacSwb3k615z7mHy7nqBUkOruZ9E # tnvC2qla+uL3ks91O/e/LnKzH9Lj1JmEBf6jwPN/MYR9Dymni4Mi3AQ8mpQMyFmi # XcSHymibSNbtTMavpdBWjFfrcvPETX7krROUOoLzMQmNgHArceSh55tgvDRdSU5c # WK3BTvK3l3mgCdgjre7XGYxV3W8apyxk5+RKfHdbv9cpRwpSuDnI8sHeqmB3fnfo # Cr1PPu4WhKegt20CobhDVybiBdhDVqUdR53ful4N/coQOEHDrIExB5nJf9Pvdrza # DyIGKAMIXD79ba5/rQEo+2cA66oJkPlvB5hEGI/jtDcYwDBgalbwB7Kc8zAAhl6+ # JvHfYpXOkppSfEQbaRXZI+LGXWQAFa5pJDfDEAyZSXprStgw594sWUOysp+UOxFe # kSA4mBr0o1jVpdaulwIDAQABo4ICAzCCAf8wHwYDVR0jBBgwFoAUaDfg67Y7+F8R # hvv+YXsIiGX0TkIwHQYDVR0OBBYEFAmyTB5bcWyA+8+rq540jPRLJ1nYMD4GA1Ud # IAQ3MDUwMwYGZ4EMAQQBMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNl # cnQuY29tL0NQUzAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMw # gbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0Rp # Z2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5j # cmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0 # ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNIQTM4NDIwMjFDQTEuY3JsMIGUBggrBgEF # BQcBAQSBhzCBhDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29t # MFwGCCsGAQUFBzAChlBodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNl # cnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJ # BgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQAh2Jnt9IPoBvOQlYQUlCP9iJ5y # XAvEWe1camOwedqMZsHEPpT2yd6+fMzPZmV3/bYJgaN2OrDS1snf62S7yc+AulVw # PAXSp1lSAiFEbZ6PFEdEBIag9B65Mp/cvRtJsIWQIc//jWqFMHpkU6r3MW9YARRu # vaIf5/0qlM4VEwn3lTf+jJdxhhyoOFTWWd3BrlMPcT06z6F6hFfyycQkZ3Y9wEJ3 # uOU9bCNLZL1HCjlKT+oI0WsgeRdbe2sYrnvv9NmDY9oEi8PEq+DGjiTgLbY5OcAX # uUogPPw6gbcuNn8Hq6FFKPIQxaksB8dF8Gw4m2lQoUWESPRF8Zaq9lmZN3+QzA79 # yskfJtAFqz3gUP5wJBdNfi/u1sGbLI0QnJQkIKfFuz7DfDPldw0gIl05BIYwZBmj # TpFRu1/+gIlP1Ul4L/wt9Lxk6pglObLsdxHP2UQrG30JaUN0gv3xZMBBByHGVVTe # cyU4qwJ0ulMdv/kjHwh+m58uOF8gHXLfyBmOjYpohN3+l0rS0qdArZMNSmLTA7N8 # n3V3AZLKB//1yhPt++gR4pCFdXmgwYDDLRxjlV0cMsG1UeSQUdI0aieh/grg5TQO # CergVXS5h3sz5U0ZQPWND41LJhA0gF2OGZNHdUc9+0dwTsfxAERrjaTdeZp0/rdZ # 9iGBoiRsS4U86S8xkDGCGnMwghpvAgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNV # BAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0 # IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENBMQIQDZEPHT770dKt # 2LbeB9E92jANBglghkgBZQMEAgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgACh # AoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAM # BgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCAuuty8SszBOw9GUcvDDIcsqCef # mOr1BV+QaMYtrJveezANBgkqhkiG9w0BAQEFAASCAgAOQ+OFSqMDsxoOERQ6/dSe # /sbxJieeKJlkOKcs4euHW2HONhTmPFZgScsTWjMroRBT9lLRZRrXgWW4s2Lfgdoa # O6uZeVqBLsVKxeBSMKXcKNRB4Bu6or7m4M5LR+dhe1/j7jd9nTDTrE6XiqX64mi8 # QRi6w8R9sJKWC/73vc7l+gAHGwkkP5cpkPzdqmQH3DiPaxvsYyPdVHPMXZzhvS3R # q7Iip9CnzI4HG7wICH64m9NDX7BYyPCm+360wdq5eYeJ7P7hAcIJq3Z43wVSeju6 # iMt12PQqgAw4hJrilWepkGPmBggYQMdB5UcH3mUWKKQiMHvFVNersavwOEscJ50D # QvRC30tPPQwoqq++pg66mdcgyXQ+8g8ERsLnJ8USNq90pKy93MWmorgTs9qPWgKm # 8UvZYizao9Bw58i2++hVUcQkUz8XHAlom5+3Aa9CE//SrFfa9QBKhvWh0Mn7/50b # eOJGdZccETnSwVZL74foirHAtIrzqnnuuwPtjq4q9xJBzpl7bLEG0QsRnwmL8ali # OqB+7s6tKSZ1teoWkdAh/xVTI9MLd+uStpwGOpdknYnmzP10zSclY3t8u5FBlsFC # Nld6G3Smv1IQX5Vm4NheJeLrWumJNWUTDIrmYleW2IlKltLTCtKc8aL6pAmRUler # KrRkAozVkNzfQ0kbiKqgA6GCF0Awghc8BgorBgEEAYI3AwMBMYIXLDCCFygGCSqG # SIb3DQEHAqCCFxkwghcVAgEDMQ8wDQYJYIZIAWUDBAIBBQAweAYLKoZIhvcNAQkQ # AQSgaQRnMGUCAQEGCWCGSAGG/WwHATAxMA0GCWCGSAFlAwQCAQUABCBICP+nYKZz # DmNGWpvKzqVe2Kq6J8cAuOC4F8eua2h8TQIRAMXXTMpEzz0BuNonLrkVV8gYDzIw # MjQwNjEyMTMxNDQxWqCCEwkwggbCMIIEqqADAgECAhAFRK/zlJ0IOaa/2z9f5WEW # MA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2Vy # dCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNI # QTI1NiBUaW1lU3RhbXBpbmcgQ0EwHhcNMjMwNzE0MDAwMDAwWhcNMzQxMDEzMjM1 # OTU5WjBIMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xIDAe # BgNVBAMTF0RpZ2lDZXJ0IFRpbWVzdGFtcCAyMDIzMIICIjANBgkqhkiG9w0BAQEF # AAOCAg8AMIICCgKCAgEAo1NFhx2DjlusPlSzI+DPn9fl0uddoQ4J3C9Io5d6Oyqc # Z9xiFVjBqZMRp82qsmrdECmKHmJjadNYnDVxvzqX65RQjxwg6seaOy+WZuNp52n+ # W8PWKyAcwZeUtKVQgfLPywemMGjKg0La/H8JJJSkghraarrYO8pd3hkYhftF6g1h # bJ3+cV7EBpo88MUueQ8bZlLjyNY+X9pD04T10Mf2SC1eRXWWdf7dEKEbg8G45lKV # tUfXeCk5a+B4WZfjRCtK1ZXO7wgX6oJkTf8j48qG7rSkIWRw69XloNpjsy7pBe6q # 9iT1HbybHLK3X9/w7nZ9MZllR1WdSiQvrCuXvp/k/XtzPjLuUjT71Lvr1KAsNJvj # 3m5kGQc3AZEPHLVRzapMZoOIaGK7vEEbeBlt5NkP4FhB+9ixLOFRr7StFQYU6mII # E9NpHnxkTZ0P387RXoyqq1AVybPKvNfEO2hEo6U7Qv1zfe7dCv95NBB+plwKWEwA # PoVpdceDZNZ1zY8SdlalJPrXxGshuugfNJgvOuprAbD3+yqG7HtSOKmYCaFxsmxx # rz64b5bV4RAT/mFHCoz+8LbH1cfebCTwv0KCyqBxPZySkwS0aXAnDU+3tTbRyV8I # pHCj7ArxES5k4MsiK8rxKBMhSVF+BmbTO77665E42FEHypS34lCh8zrTioPLQHsC # AwEAAaOCAYswggGHMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMBYGA1Ud # JQEB/wQMMAoGCCsGAQUFBwMIMCAGA1UdIAQZMBcwCAYGZ4EMAQQCMAsGCWCGSAGG # /WwHATAfBgNVHSMEGDAWgBS6FtltTYUvcyl2mi91jGogj57IbzAdBgNVHQ4EFgQU # pbbvE+fvzdBkodVWqWUxo97V40kwWgYDVR0fBFMwUTBPoE2gS4ZJaHR0cDovL2Ny # bDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0UlNBNDA5NlNIQTI1NlRp # bWVTdGFtcGluZ0NBLmNybDCBkAYIKwYBBQUHAQEEgYMwgYAwJAYIKwYBBQUHMAGG # GGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBYBggrBgEFBQcwAoZMaHR0cDovL2Nh # Y2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0UlNBNDA5NlNIQTI1 # NlRpbWVTdGFtcGluZ0NBLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAgRrW3qCptZgX # vHCNT4o8aJzYJf/LLOTN6l0ikuyMIgKpuM+AqNnn48XtJoKKcS8Y3U623mzX4WCc # K+3tPUiOuGu6fF29wmE3aEl3o+uQqhLXJ4Xzjh6S2sJAOJ9dyKAuJXglnSoFeoQp # mLZXeY/bJlYrsPOnvTcM2Jh2T1a5UsK2nTipgedtQVyMadG5K8TGe8+c+njikxp2 # oml101DkRBK+IA2eqUTQ+OVJdwhaIcW0z5iVGlS6ubzBaRm6zxbygzc0brBBJt3e # WpdPM43UjXd9dUWhpVgmagNF3tlQtVCMr1a9TMXhRsUo063nQwBw3syYnhmJA+rU # kTfvTVLzyWAhxFZH7doRS4wyw4jmWOK22z75X7BC1o/jF5HRqsBV44a/rCcsQdCa # M0qoNtS5cpZ+l3k4SF/Kwtw9Mt911jZnWon49qfH5U81PAC9vpwqbHkB3NpE5jre # ODsHXjlY9HxzMVWggBHLFAx+rrz+pOt5Zapo1iLKO+uagjVXKBbLafIymrLS2Dq4 # sUaGa7oX/cR3bBVsrquvczroSUa31X/MtjjA2Owc9bahuEMs305MfR5ocMB3CtQC # 4Fxguyj/OOVSWtasFyIjTvTs0xf7UGv/B3cfcZdEQcm4RtNsMnxYL2dHZeUbc7aZ # +WssBkbvQR7w8F/g29mtkIBEr4AQQYowggauMIIElqADAgECAhAHNje3JFR82Ees # /ShmKl5bMA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxE # aWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMT # GERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAz # MjIyMzU5NTlaMGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5j # LjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBU # aW1lU3RhbXBpbmcgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDG # hjUGSbPBPXJJUVXHJQPE8pE3qZdRodbSg9GeTKJtoLDMg/la9hGhRBVCX6SI82j6 # ffOciQt/nR+eDzMfUBMLJnOWbfhXqAJ9/UO0hNoR8XOxs+4rgISKIhjf69o9xBd/ # qxkrPkLcZ47qUT3w1lbU5ygt69OxtXXnHwZljZQp09nsad/ZkIdGAHvbREGJ3Hxq # V3rwN3mfXazL6IRktFLydkf3YYMZ3V+0VAshaG43IbtArF+y3kp9zvU5EmfvDqVj # bOSmxR3NNg1c1eYbqMFkdECnwHLFuk4fsbVYTXn+149zk6wsOeKlSNbwsDETqVcp # licu9Yemj052FVUmcJgmf6AaRyBD40NjgHt1biclkJg6OBGz9vae5jtb7IHeIhTZ # girHkr+g3uM+onP65x9abJTyUpURK1h0QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZG # s506o9UD4L/wojzKQtwYSH8UNM/STKvvmz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHz # NklNiyDSLFc1eSuo80VgvCONWPfcYd6T/jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2 # ElGTyYwMO1uKIqjBJgj5FBASA31fI7tk42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJ # ASgADoRU7s7pXcheMBK9Rp6103a50g5rmQzSM7TNsQIDAQABo4IBXTCCAVkwEgYD # VR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUuhbZbU2FL3MpdpovdYxqII+eyG8w # HwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGG # MBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcw # AYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8v # Y2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBD # BgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNl # cnRUcnVzdGVkUm9vdEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgB # hv1sBwEwDQYJKoZIhvcNAQELBQADggIBAH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4Q # TRPPMFPOvxj7x1Bd4ksp+3CKDaopafxpwc8dB+k+YMjYC+VcW9dth/qEICU0MWfN # thKWb8RQTGIdDAiCqBa9qVbPFXONASIlzpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1g # tqpPkWaeLJ7giqzl/Yy8ZCaHbJK9nXzQcAp876i8dU+6WvepELJd6f8oVInw1Ypx # dmXazPByoyP6wCeCRK6ZJxurJB4mwbfeKuv2nrF5mYGjVoarCkXJ38SNoOeY+/um # nXKvxMfBwWpx2cYTgAnEtp/Nh4cku0+jSbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+U # zTl63f8lY5knLD0/a6fxZsNBzU+2QJshIUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhz # q6YBT70/O3itTK37xJV77QpfMzmHQXh6OOmc4d0j/R0o08f56PGYX/sr2H7yRp11 # LB4nLCbbbxV7HhmLNriT1ObyF5lZynDwN7+YAN8gFk8n+2BnFqFmut1VwDophrCY # oCvtlUG3OtUVmDG0YgkPCr2B2RP+v6TR81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvk # dgIm2fBldkKmKYcJRyvmfxqkhQ/8mJb2VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3 # OBqhK/bt1nz8MIIFjTCCBHWgAwIBAgIQDpsYjvnQLefv21DiCEAYWjANBgkqhkiG # 9w0BAQwFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkw # FwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1 # cmVkIElEIFJvb3QgQ0EwHhcNMjIwODAxMDAwMDAwWhcNMzExMTA5MjM1OTU5WjBi # MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 # d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg # RzQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAi # MGkz7MKnJS7JIT3yithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnny # yhHS5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE # 5nQ7bXHiLQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm # 7nfISKhmV1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5 # w3jHtrHEtWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsD # dV14Ztk6MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1Z # XUJ2h4mXaXpI8OCiEhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS0 # 0mFt6zPZxd9LBADMfRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hk # pjPRiQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m8 # 00ERElvlEFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+i # sX4KJpn15GkvmB0t9dmpsh3lGwIDAQABo4IBOjCCATYwDwYDVR0TAQH/BAUwAwEB # /zAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wHwYDVR0jBBgwFoAUReui # r/SSy4IxLVGLp6chnfNtyA8wDgYDVR0PAQH/BAQDAgGGMHkGCCsGAQUFBwEBBG0w # azAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsGAQUF # BzAChjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVk # SURSb290Q0EuY3J0MEUGA1UdHwQ+MDwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2lj # ZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwEQYDVR0gBAowCDAG # BgRVHSAAMA0GCSqGSIb3DQEBDAUAA4IBAQBwoL9DXFXnOF+go3QbPbYW1/e/Vwe9 # mqyhhyzshV6pGrsi+IcaaVQi7aSId229GhT0E0p6Ly23OO/0/4C5+KH38nLeJLxS # A8hO0Cre+i1Wz/n096wwepqLsl7Uz9FDRJtDIeuWcqFItJnLnU+nBgMTdydE1Od/ # 6Fmo8L8vC6bp8jQ87PcDx4eo0kxAGTVGamlUsLihVo7spNU96LHc/RzY9HdaXFSM # b++hUD38dglohJ9vytsgjTVgHAIDyyCwrFigDkBjxZgiwbJZ9VVrzyerbHbObyMt # 9H5xaiNrIv8SuFQtJ37YOtnwtoeW/VvRXKwYw02fc7cBqZ9Xql4o4rmUMYIDdjCC # A3ICAQEwdzBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4x # OzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGlt # ZVN0YW1waW5nIENBAhAFRK/zlJ0IOaa/2z9f5WEWMA0GCWCGSAFlAwQCAQUAoIHR # MBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjQw # NjEyMTMxNDQxWjArBgsqhkiG9w0BCRACDDEcMBowGDAWBBRm8CsywsLJD4JdzqqK # ycZPGZzPQDAvBgkqhkiG9w0BCQQxIgQgTmCV8oWJFcs+BFN8+4yDQ1YRwJG8jxEA # MurxMufsDGswNwYLKoZIhvcNAQkQAi8xKDAmMCQwIgQg0vbkbe10IszR1EBXaEE2 # b4KK2lWarjMWr00amtQMeCgwDQYJKoZIhvcNAQEBBQAEggIAAhC/igGTGY+VGIVJ # VIxFol8Np8uVBLzqRyya0i34e6eBnobateSwxzLp2Ukd8yeIrpX7HSyu1AAU/c0s # 2AMMKzdwaWSabfSzt1cjwDVNak9/0Sb63itdhYqCAbMKOd4RkKfL/0Yuqz9vsOvs # 9svA13RJ5YYzVB1Dk7NXLJ37cUQ5E5ibAMfDS232t6X1JGz7+dxiHnU6dGD3nQua # oMlC6bheBzdDv6OnJp5bQ9q3g65vSeMUB9mS/CGMoTvKo06ywjkxwDkP/LFtGRJg # oxA60/Mh0T9N2vvzaupxYG4Xhn8i6V88AA5wgaUJHYUuckY82EJdWDwEXr3matLn # 7p9H0aAfCMG7W7x+yhFLnl4/uiEjfcFdT/Ml36E6KgzJANDCo56UCF8scrL2Hk2n # gmVUTT+VkjnEACWgbD/EIOk8WTdzD/wrhjmKVK+r95XKGV3RRVp42PKDVEIXn+zA # AawFS4MKyDU8uqLYIJTalF4iC8eEA5FmimfJpsJf7+Ihmdp2QfYBxDAGvYp3YkED # LilL31yvfdWQJ3AAC02JDzr8PB/q47dqb3DcJrLp/R+H+lqJmmZ+MSfvrFmSmFdz # HEf4pNQm0JNfRpVx7VcGmrDQ23Z0GYTQx5+LqDbw8p4y9mdNdmKEsJS0kd+pVHUG # VjbdboK0vnyGKCcHrR+PH2we1O0= # SIG # End signature block |