Modules/Support/Support.psm1
using module '..\ScubaConfig\ScubaConfig.psm1' function Copy-SCuBABaselineDocument { <# .SYNOPSIS Copy security baselines documents to a user specified location. .Description This function makes copies of the security baseline documents included with the installed ScubaGear module. .Parameter Destination Where to copy the baselines. Defaults to <user home>\ScubaGear\baselines .Example Copy-SCuBABaselineDocument .Functionality Public .NOTES SuppressMessage for PSReviewUnusedParameter due to linter bug. Open issue to remove if/when fixed. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] param ( [Parameter(Mandatory = $false)] [ValidateScript({Test-Path -Path $_ -IsValid})] [string] $Destination = (Join-Path -Path $env:USERPROFILE -ChildPath "ScubaGear"), [Parameter(Mandatory = $false)] [switch] $Force ) if (-not (Test-Path -Path $Destination -PathType Container)){ New-Item -ItemType Directory -Path $Destination | Out-Null } @("teams", "exo", "defender", "aad", "powerbi", "powerplatform", "sharepoint") | ForEach-Object { $SourceFileName = Join-Path -Path $PSScriptRoot -ChildPath "..\..\baselines\$_.md" $TargetFileName = Join-Path -Path $Destination -ChildPath "$_.md" Copy-Item -Path $SourceFileName -Destination $Destination -Force:$Force -ErrorAction Stop 2> $null Set-ItemProperty -Path $TargetFileName -Name IsReadOnly -Value $true } } function Initialize-SCuBA { <# .SYNOPSIS This script installs the required Powershell modules used by the assessment tool .DESCRIPTION Installs the modules required to support SCuBAGear. If the Force switch is set then any existing module will be re-installed even if already at latest version. If the SkipUpdate switch is set then any existing module will not be updated to th latest version. .EXAMPLE Initialize-SCuBA .EXAMPLE Initalize-SCuBA -Scope AllUsers Install all dependent PowerShell modules in a location that's accessible to all users of the computer. .NOTES Executing the script with no switches set will install the latest version of a module if not already installed. #> [CmdletBinding()] param( [Parameter(Mandatory = $false, HelpMessage = 'Installs a given module and overrides warning messages about module installation conflicts. If a module with the same name already exists on the computer, Force allows for multiple versions to be installed. If there is an existing module with the same name and version, Force overwrites that version')] [switch] $Force, [Parameter(HelpMessage = 'If specified then modules will not be updated to latest version')] [switch] $SkipUpdate, [Parameter(HelpMessage = 'Do not automatically trust the PSGallery repository for module installation')] [switch] $DoNotAutoTrustRepository, [Parameter(HelpMessage = 'Do not download OPA')] [switch] $NoOPA, [Parameter(Mandatory = $false, HelpMessage = 'The version of OPA Rego to be downloaded, must be in "x.x.x" format')] [Alias('version')] [string] $ExpectedVersion = [ScubaConfig]::ScubaDefault('DefaultOPAVersion'), [Parameter(Mandatory = $false, HelpMessage = 'The operating system the program is running on')] [ValidateSet('Windows','MacOS','Linux')] [Alias('os')] [string] $OperatingSystem = "Windows", [Parameter(Mandatory = $false, HelpMessage = 'The file name that the opa executable is to be saved as')] [Alias('name')] [string] $OPAExe = "", [Parameter(Mandatory=$false, HelpMessage = 'Directory to contain ScubaGear artifacts. Defaults to <home>.')] [ValidateScript({Test-Path -Path $_ -PathType Container})] [string] $ScubaParentDirectory = $env:USERPROFILE, [Parameter(Mandatory=$false, HelpMessage = 'Specifies the Install-Module scope of the dependent PowerShell modules. Acceptable values are AllUsers and CurrentUser. Defaults to CurrentUser')] [ValidateSet('CurrentUser','AllUsers')] [string] $Scope = 'CurrentUser' ) Write-Output 'Initializing ScubaGear...' # Set preferences for writing messages $PreferenceStack = New-Object -TypeName System.Collections.Stack $PreferenceStack.Push($DebugPreference) $PreferenceStack.Push($InformationPreference) $DebugPreference = "Continue" $InformationPreference = "Continue" if (-not $DoNotAutoTrustRepository) { $Policy = Get-PSRepository -Name "PSGallery" | Select-Object -Property -InstallationPolicy if ($($Policy.InstallationPolicy) -ne "Trusted") { Set-PSRepository -Name "PSGallery" -InstallationPolicy Trusted Write-Information -MessageData "Setting PSGallery repository to trusted." } } # Start a stopwatch to time module installation elapsed time $Stopwatch = [System.Diagnostics.Stopwatch]::StartNew() # Since this method is called from the Support module, script root # points to the Support module location # Use script root rather than Get-Module as this may be called by # a script that only imports the function and not the whole module $SupportPath = $PSScriptRoot # Scuba module structure means module home is grandparent of support $ScubaModuleDir = Split-Path -Path $(Split-Path -Path $SupportPath -Parent) -Parent # Removing the import below causes issues with testing, let it be. # Import module magic may be helping by: # * restricting the import so only that only function is exported # * imported function takes precedence over imported modules w/ function Import-Module $SupportPath -Function Initialize-Scuba try { ($RequiredModulesPath = Join-Path -Path $ScubaModuleDir -ChildPath 'RequiredVersions.ps1') *> $null . $RequiredModulesPath } catch { throw "Unable to find RequiredVersions.ps1 in expected directory:`n`t$ScubaModuleDir" } if ($ModuleList) { # Add PowerShellGet to beginning of ModuleList for installing required modules. $ModuleList = ,@{ ModuleName = 'PowerShellGet' ModuleVersion = [version] '2.1.0' MaximumVersion = [version] '2.99.99999' } + $ModuleList } else { throw "Required modules list is required." } foreach ($Module in $ModuleList) { $ModuleName = $Module.ModuleName if (Get-Module -ListAvailable -Name $ModuleName) { $HighestInstalledVersion = (Get-Module -ListAvailable -Name $ModuleName | Sort-Object Version -Descending | Select-Object Version -First 1).Version $LatestVersion = [Version](Find-Module -Name $ModuleName -MinimumVersion $Module.ModuleVersion -MaximumVersion $Module.MaximumVersion).Version if ($HighestInstalledVersion -ge $LatestVersion) { Write-Debug "${ModuleName}: ${HighestInstalledVersion} already has latest installed." if ($Force -eq $true) { Install-Module -Name $ModuleName ` -Force ` -AllowClobber ` -Scope "$($Scope)" ` -MaximumVersion $Module.MaximumVersion Write-Information -MessageData "Re-installing module to latest acceptable version: ${ModuleName}." } } else { if ($SkipUpdate -eq $true) { Write-Debug "Skipping update for ${ModuleName}: ${HighestInstalledVersion} to newer version ${LatestVersion}." } else { Install-Module -Name $ModuleName ` -Force ` -AllowClobber ` -Scope "$($Scope)" ` -MaximumVersion $Module.MaximumVersion $MaxInstalledVersion = (Get-Module -ListAvailable -Name $ModuleName | Sort-Object Version -Descending | Select-Object Version -First 1).Version Write-Information -MessageData "${ModuleName}: ${HighestInstalledVersion} updated to version ${MaxInstalledVersion}." } } } else { Install-Module -Name $ModuleName ` -AllowClobber ` -Scope "$($Scope)" ` -MaximumVersion $Module.MaximumVersion $MaxInstalledVersion = (Get-Module -ListAvailable -Name $ModuleName | Sort-Object Version -Descending | Select-Object Version -First 1).Version Write-Information -MessageData "Installed the latest acceptable version of ${ModuleName}: ${MaxInstalledVersion}." } } if ($NoOPA -eq $true) { Write-Debug "Skipping Download for OPA.`n" } else { try { Install-OPAforSCuBA -OPAExe $OPAExe -ExpectedVersion $ExpectedVersion -OperatingSystem $OperatingSystem -ScubaParentDirectory $ScubaParentDirectory } catch { $Error[0] | Format-List -Property * -Force | Out-Host } } # Stop the clock and report total elapsed time $Stopwatch.stop() Write-Debug "ScubaGear setup time elapsed: $([math]::Round($stopwatch.Elapsed.TotalSeconds,0)) seconds." $InformationPreference = $PreferenceStack.Pop() $DebugPreference = $PreferenceStack.Pop() } function Install-OPAforSCuBA { <# .SYNOPSIS This script installs the required OPA executable used by the assessment tool .DESCRIPTION Installs the OPA executable required to support SCuBAGear. .EXAMPLE Install-OPAforSCuBA #> [CmdletBinding()] param( [Parameter(Mandatory = $false, HelpMessage = 'The version of OPA Rego to be downloaded, must be in "x.x.x" format')] [Alias('version')] [string] $ExpectedVersion = [ScubaConfig]::ScubaDefault('DefaultOPAVersion'), [Parameter(Mandatory = $false, HelpMessage = 'The file name that the opa executable is to be saved as')] [Alias('name')] [string] $OPAExe = "", [Parameter(Mandatory = $false, HelpMessage = 'The operating system the program is running on')] [ValidateSet('Windows','MacOS','Linux')] [Alias('os')] [string] $OperatingSystem = "Windows", [Parameter(Mandatory=$false, HelpMessage = 'Directory to contain ScubaGear artifacts. Defaults to <home>.')] [ValidateScript({Test-Path -Path $_ -PathType Container})] [string] $ScubaParentDirectory = $env:USERPROFILE ) # Constants $ACCEPTABLEVERSIONS = '0.69.0', [ScubaConfig]::ScubaDefault('DefaultOPAVersion') # End Versions $FILENAME = @{ Windows = "opa_windows_amd64.exe"; MacOS = "opa_darwin_amd64"; Linux = "opa_linux_amd64_static"} # Set prefernces for writing messages $PreferenceStack = New-Object -TypeName System.Collections.Stack $PreferenceStack.Push($DebugPreference) $PreferenceStack.Push($InformationPreference) $PreferenceStack.Push($ErrorActionPreference) $DebugPreference = "Continue" $InformationPreference = "Continue" $ErrorActionPreference = "Stop" $ScubaHiddenHome = Join-Path -Path $ScubaParentDirectory -ChildPath '.scubagear' $ScubaTools = Join-Path -Path $ScubaHiddenHome -ChildPath 'Tools' if((Test-Path -Path $ScubaTools) -eq $false) { New-Item -ItemType Directory -Force -Path $ScubaTools Write-Output "" | Out-Host } if(-not $ACCEPTABLEVERSIONS.Contains($ExpectedVersion)) { $AcceptableVersionsString = $ACCEPTABLEVERSIONS -join "`r`n" | Out-String throw "Version parameter entered, ${ExpectedVersion}, is not in the list of acceptable versions. Acceptable versions are:`r`n${AcceptableVersionsString}" } $Filename = $FILENAME.$OperatingSystem if($OPAExe -eq "") { $OPAExe = $Filename } if(Test-Path -Path ( Join-Path $ScubaTools $OPAExe) -PathType Leaf) { $Result = Confirm-OPAHash -out $OPAExe -version $ExpectedVersion -name $Filename if($Result[0]) { Write-Debug "${OPAExe}: ${ExpectedVersion} already has latest installed." } else { if($OPAExe -eq $Filename) { Write-Information "SHA256 verification failed, downloading new executable" | Out-Host InstallOPA -out $OPAExe -version $ExpectedVersion -name $Filename } else { Write-Warning "SHA256 verification failed, please confirm file name is correct & remove old file before running script" | Out-Host } } } else { InstallOPA -out $OPAExe -version $ExpectedVersion -name $Filename } $ErrorActionPreference = $PreferenceStack.Pop() $InformationPreference = $PreferenceStack.Pop() $DebugPreference = $PreferenceStack.Pop() } function Get-OPAFile { param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [Alias('out')] [string]$OPAExe, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [Alias('version')] [string]$ExpectedVersion, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [Alias('name')] [string]$Filename ) <# .FUNCTIONALITY Internal #> $InstallUrl = "https://openpolicyagent.org/downloads/v$($ExpectedVersion)/$($Filename)" $OutFile = ( Join-Path $ScubaTools $OPAExe ) #(Join-Path (Get-Location).Path $OPAExe) try { $Display = "Downloading OPA executable" Start-BitsTransfer -Source $InstallUrl -Destination $OutFile -DisplayName $Display -MaxDownloadTime 300 Write-Information -MessageData "Installed the specified OPA version (${ExpectedVersion}) to ${OutFile}" | Out-Host } catch { $Error[0] | Format-List -Property * -Force | Out-Host throw "Unable to download OPA executable. To try manually downloading, see details in README under 'Download the required OPA executable'" | Out-Host } } function Get-ExeHash { param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [Alias('name')] [string]$Filename, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [Alias('version')] [string]$ExpectedVersion ) <# .FUNCTIONALITY Internal #> $InstallUrl = "https://openpolicyagent.org/downloads/v$($ExpectedVersion)/$($Filename).sha256" $OutFile = (Join-Path (Get-Location).Path $InstallUrl.SubString($InstallUrl.LastIndexOf('/'))) try { $WebClient = New-Object System.Net.WebClient $WebClient.DownloadFile($InstallUrl, $OutFile) } catch { $Error[0] | Format-List -Property * -Force | Out-Host Write-Error "Unable to download OPA SHA256 hash for verification" | Out-Host } finally { $WebClient.Dispose() } $Hash = ($(Get-Content $OutFile -raw) -split " ")[0] Remove-Item $OutFile return $Hash } function Confirm-OPAHash { <# .FUNCTIONALITY Internal #> param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [Alias('out')] [string]$OPAExe, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [Alias('version')] [string]$ExpectedVersion, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [Alias('name')] [string] $Filename ) if ((Get-FileHash ( Join-Path $ScubaTools $OPAExe ) -Algorithm SHA256 ).Hash -ne $(Get-ExeHash -name $Filename -version $ExpectedVersion)) { return $false, "SHA256 verification failed, retry download or install manually. See README under 'Download the required OPA executable' for instructions." } return $true, "Downloaded OPA version ($ExpectedVersion) SHA256 verified successfully`n" } function InstallOPA { param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [Alias('out')] [string]$OPAExe, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [Alias('version')] [string]$ExpectedVersion, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [Alias('name')] [string] $Filename ) <# .FUNCTIONALITY Internal #> Get-OPAFile -out $OPAExe -version $ExpectedVersion -name $Filename $Result = Confirm-OPAHash -out $OPAExe -version $ExpectedVersion -name $Filename $Result[1] | Out-Host } function Debug-SCuBA { <# .SYNOPSIS Gather diagnostic information from previous run(s) into a single archive bundle for error reporting and troubleshooting. .DESCRIPTION Assists development teams in diagnosing issues with the ScubaGear assessment tool by generating and bundling up information related to one or more previous assessment runs. .EXAMPLE Debug-SCuBA .NOTES Executing the script with no switches will cause it to create an archive of the latest SCuBAGear run report and result files in the current working directory. #> [CmdletBinding()] param ( [Parameter(Mandatory=$false, HelpMessage = 'Directory to contain debug report')] [string] $ReportPath = "$($($(Get-Item $PSScriptRoot).Parent).FullName)\Reports", [Parameter(Mandatory=$false, HelpMessage = 'Include ScubaGear report on tenant configuration?')] [switch] $IncludeReports = $false, [Parameter(Mandatory=$false, HelpMessage = 'Include all available ScubaGear report on tenant configuration?')] [switch] $AllReports = $false ) $PreferenceStack = New-Object -TypeName System.Collections.Stack $PreferenceStack.Push($DebugPreference) $DebugPreference = 'Continue' # Set registry key to inspect $regPath = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WinRM\Client' $regKey = 'AllowBasic' $Timestamp = Get-Date -Format yyyyMMdd_HHmmss Write-Debug "Script started from $PSScriptRoot" Write-Debug "Report Path is $ReportPath" Write-Debug "Timestamp set as $Timestamp" ## Create bundle directory timestamped inside current directory try { $DiagnosticPath = New-Item -ItemType Directory "ScubaGear_diag_$Timestamp" Write-Debug "Created new directory $($DiagnosticPath.FullName)" $EnvFile= New-Item -Path $(Join-Path -Path $DiagnosticPath -ChildPath EnvInfo_$Timestamp) -ItemType File Write-Debug "Created new environment info file at $($EnvFile.FullName)" } catch { Write-Error "ERRROR: Could not create diagnostics directory and/or files." } ## Get environment information "System Environment information from $Timestamp`n" >> $EnvFile "PowerShell Information" >> $EnvFile "----------------------" >> $EnvFile $PSVersionTable >> $EnvFile "`n" >> $EnvFile "WinRM Client Setting" >> $EnvFile "--------------------" >> $EnvFile if (Test-Path -LiteralPath $regPath){ try { $allowBasic = Get-ItemPropertyValue -Path $regPath -Name $regKey } catch [System.Management.Automation.PSArgumentException]{ "Key, $regKey, was not found`n" >> $EnvFile } catch{ "Unexpected error occured attempting to get registry key, $regKey.`n" >> $EnvFile } "AllowBasic = $allowBasic`n" >> $EnvFile } else { "Registry path not found: $regPath" >> $EnvFile } "Installed PowerShell Modules Available" >> $EnvFile "--------------------------------------" >> $EnvFile Get-Module -ListAvailable >> $EnvFile "Imported PowerShell Modules" >> $EnvFile "---------------------------" >> $EnvFile Get-Module >> $EnvFile if($IncludeReports) { # Generate list of ScubaGear Report folder(s) to include in diagnostics $ReportList = @() if($AllReports) { $ReportList = Get-ChildItem -Directory -Path $ReportPath -Filter "M365BaselineConformance*" } else { $ReportList = Get-ChildItem -Directory -Path $ReportPath -Filter "M365BaselineConformance*" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 } Write-Debug "Reports to Include: $ReportList" if($ReportList.Count -eq 0) { Write-Warning "No ScubaGear report folders found at $ReportPath." } # Copy each report folder to diagnostics folder foreach ($ReportFolder in $ReportList) { Write-Debug "Copying $($ReportFolder.FullName) to diagnostic bundle" Copy-Item -Path $ReportFolder.FullName -Destination $DiagnosticPath -Recurse } } # Create archive bundle of report and results directory $ZipFile = "$($DiagnosticPath.FullName).zip" if(Test-Path -Path $ZipFile) { Write-Error "ERROR: Diagnostic archive bundle $ZipFile already exists" } else { Compress-Archive -Path $DiagnosticPath.FullName -DestinationPath $ZipFile } $DebugPreference = $PreferenceStack.Pop() } function Copy-SCuBASampleReport { <# .SYNOPSIS Copy sample reports to user defined location. .Description This function makes copies of the sample reports included with the installed ScubaGear module. .Parameter Destination Where to copy the samples. Defaults to <user home>\ScubaGear\samples\reports .Example Copy-SCuBASampleReport .Functionality Public .NOTES SuppressMessage for PSReviewUnusedParameter due to linter bug. Open issue to remove if/when fixed. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] param ( [Parameter(Mandatory = $false)] [ValidateScript({Test-Path -Path $_ -IsValid})] [string] $DestinationDirectory = (Join-Path -Path $env:USERPROFILE -ChildPath "ScubaGear/samples/reports"), [Parameter(Mandatory = $false)] [switch] $Force ) $SourceDirectory = Join-Path -Path $PSScriptRoot -ChildPath "..\..\Sample-Reports\" Copy-ScubaModuleFile -SourceDirectory $SourceDirectory -DestinationDirectory $DestinationDirectory -Force:$Force } function Copy-SCuBASampleConfigFile { <# .SYNOPSIS Copy sample configuration files to user defined location. .Description This function makes copies of the sample configuration files included with the installed ScubaGear module. .Parameter Destination Where to copy the samples. Defaults to <user home>\ScubaGear\samples\config-files .Example Copy-SCuBASampleConfigFile .Functionality Public .NOTES SuppressMessage for PSReviewUnusedParameter due to linter bug. Open issue to remove if/when fixed. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] param ( [Parameter(Mandatory = $false)] [ValidateScript({Test-Path -Path $_ -IsValid})] [string] $DestinationDirectory = (Join-Path -Path $env:USERPROFILE -ChildPath "ScubaGear/samples/config-files"), [Parameter(Mandatory = $false)] [switch] $Force ) $SourceDirectory = Join-Path -Path $PSScriptRoot -ChildPath "..\..\Sample-Config-Files\" Copy-ScubaModuleFile -SourceDirectory $SourceDirectory -DestinationDirectory $DestinationDirectory -Force:$Force } function Copy-ScubaModuleFile { <# .SYNOPSIS Copy Scuba module files (read-only) to user defined location. .Description This function makes copies of files included with the installed ScubaGear module. .Parameter Destination Where to copy the files. .Example Copy-ScubaModuleFile =Destination SomeWhere .Functionality Private .NOTES SuppressMessage for PSReviewUnusedParameter due to linter bug. Open issue to remove if/when fixed. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] param ( [Parameter(Mandatory=$true)] [ValidateScript({Test-Path -Path $_ -PathType Container})] [string] $SourceDirectory, [Parameter(Mandatory = $true)] [ValidateScript({Test-Path -Path $_ -IsValid})] [string] $DestinationDirectory, [Parameter(Mandatory = $false)] [switch] $Force ) if (-not (Test-Path -Path $DestinationDirectory -PathType Container)){ New-Item -ItemType Directory -Path $DestinationDirectory | Out-Null } try { Get-ChildItem -Path $SourceDirectory | Copy-Item -Destination $DestinationDirectory -Recurse -Container -Force:$Force -ErrorAction Stop 2> $null Get-ChildItem -Path $DestinationDirectory -File -Recurse | ForEach-Object {$_.IsReadOnly = $true} } catch { throw "Scuba copy module files failed." } } function New-SCuBAConfig { <# .SYNOPSIS Generate a config file for the ScubaGear tool .Description Using provided user input generate a config file to run ScubaGear tailored to the end user .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 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 OmitPolicy A comma-separated list of policies to exclude from the ScubaGear report, e.g., MS.DEFENDER.1.1v1. Note that the rationales will need to be manually added to the resulting config file. .Functionality Public #> [CmdletBinding(DefaultParameterSetName='Report')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] param ( [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string] $Description = "YAML configuration file with default description", #(Join-Path -Path $env:USERPROFILE -ChildPath ".scubagear\Tools"), [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", '*', IgnoreCase = $false)] [string[]] $ProductNames = @("aad", "defender", "exo", "sharepoint", "teams"), [Parameter(Mandatory = $false)] [ValidateSet("commercial", "gcc", "gcchigh", "dod", IgnoreCase = $false)] [ValidateNotNullOrEmpty()] [string] $M365Environment = "commercial", [Parameter(Mandatory = $false)] [ValidateScript({Test-Path -PathType Container $_})] [string] $OPAPath = ".", #(Join-Path -Path $env:USERPROFILE -ChildPath ".scubagear\Tools"), [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [ValidateSet($true, $false)] [boolean] $LogIn = $true, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [ValidateSet($true, $false)] [boolean] $DisconnectOnExit = $false, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string] $OutPath = '.', [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string] $AppID, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string] $CertificateThumbprint, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string] $Organization, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string] $OutFolderName = "M365BaselineConformance", [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string] $OutProviderFileName = "ProviderSettingsExport", [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string] $OutRegoFileName = "TestResults", [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string] $OutReportName = "BaselineReports", [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string] $ConfigLocation = "./", [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string[]] $OmitPolicy = @() ) $Config = New-Object ([System.Collections.specialized.OrderedDictionary]) ($MyInvocation.MyCommand.Parameters ).Keys | ForEach-Object{ $Val = (Get-Variable -Name $_ -EA SilentlyContinue).Value if( $Val.length -gt 0 ) { #$config[$_] = $val $Config.add($_, $Val) } } if ($config.Contains("OmitPolicy")) { # We don't want to immediately save this parameter to the config, as it's not in the right # format yet. $config.Remove("OmitPolicy") } $CapExclusionNamespace = @( "MS.AAD.1.1v1", "MS.AAD.2.1v1", "MS.AAD.2.3v1", "MS.AAD.3.1v1", "MS.AAD.3.2v1", "MS.AAD.3.3v1", "MS.AAD.3.6v1", "MS.AAD.3.7v1", "MS.AAD.3.8v1" ) $RoleExclusionNamespace = "MS.AAD.7.4v1" $CommonSensitiveAccountFilterNamespace = @( "MS.DEFENDER.1.4v1", "MS.DEFENDER.1.5v1" ) $UserImpersonationProtectionNamespace = "MS.DEFENDER.2.1v1" $AgencyDomainImpersonationProtectionNamespace = "MS.DEFENDER.2.2v1" $PartnerDomainImpersonationProtectionNamespace = "MS.DEFENDER.2.3v1" $OmissionNamespace = "OmitPolicy" # List to track which policies the user specified in $OmitPolicies are properly formatted $OmitPolicyValidated = @() # Hashmap to structure the ignored policies template $config[$OmissionNamespace] = @{} foreach ($Policy in $OmitPolicy) { if (-not ($Policy -match "^ms\.[a-z]+\.[0-9]+\.[0-9]+v[0-9]+$")) { # Note that -match is a case insensitive match # Note that the regex does not validate the product name, this will be done # later $Warning = "The policy, $Policy, in the OmitPolicy parameter, is not a valid " $Warning += "policy ID. Expected format 'MS.[PRODUCT].[GROUP].[NUMBER]v[VERSION]', " $Warning += "e.g., 'MS.DEFENDER.1.1v1'. Skipping." Write-Warning $Warning Continue } $Product = ($Policy -Split "\.")[1] # Here's where the product name is validated if (-not ($ProductNames -Contains $Product)) { $Warning = "The policy, $Policy, in the OmitPolicy parameter, is not encompassed by " $Warning += "the products specified in the ProductName parameter. Skipping." Write-Warning $Warning Continue } # Ensure the policy ID is properly capitalized (i.e., all caps except for the "v1" portion) $PolicyCapitalized = $Policy.Substring(0, $Policy.Length-2).ToUpper() + $Policy.SubString($Policy.Length-2) $OmitPolicyValidated += $PolicyCapitalized $config[$OmissionNamespace][$PolicyCapitalized] = @{ "Rationale" = ""; "Expiration" = ""; } } $Warning = "The following policies have been configured for omission: $($OmitPolicyValidated -Join ', '). " $Warning += "Note that as the New-Config function does not support providing the rationale for omission via " $Warning += "the commandline, you will need to open the resulting config file and manually enter the rationales." Write-Warning $Warning $AadTemplate = New-Object ([System.Collections.specialized.OrderedDictionary]) $AadCapExclusions = New-Object ([System.Collections.specialized.OrderedDictionary]) $AadRoleExclusions = New-Object ([System.Collections.specialized.OrderedDictionary]) $DefenderTemplate = New-Object ([System.Collections.specialized.OrderedDictionary]) $DefenderCommonSensitiveAccountFilter = New-Object ([System.Collections.specialized.OrderedDictionary]) #$defenderUserImpersonationProtection = New-Object ([System.Collections.specialized.OrderedDictionary]) #$defenderAgencyDomainImpersonationProtection = New-Object ([System.Collections.specialized.OrderedDictionary]) #$defenderPartnerDomainImpersonationProtection = New-Object ([System.Collections.specialized.OrderedDictionary]) $AadCapExclusions = @{ CapExclusions = @{} } $AadCapExclusions["CapExclusions"].add("Users", @("")) $AadCapExclusions["CapExclusions"].add("Groups", @("")) $AadRoleExclusions = @{ RoleExclusions = @{} } $AadRoleExclusions["RoleExclusions"].add("Users", @("")) $AadRoleExclusions["RoleExclusions"].add("Groups", @("")) foreach ($Cap in $CapExclusionNamespace){ $AadTemplate.add($Cap, $AadCapExclusions) } $AadTemplate.add($RoleExclusionNamespace, $AadRoleExclusions) $DefenderCommonSensitiveAccountFilter = @{ SensitiveAccounts = @{} } $DefenderCommonSensitiveAccountFilter['SensitiveAccounts'].add("IncludedUsers", @("")) $DefenderCommonSensitiveAccountFilter['SensitiveAccounts'].add("IncludedGroups", @("")) $DefenderCommonSensitiveAccountFilter['SensitiveAccounts'].add("IncludedDomains", @("")) $DefenderCommonSensitiveAccountFilter['SensitiveAccounts'].add("ExcludedUsers", @("")) $DefenderCommonSensitiveAccountFilter['SensitiveAccounts'].add("ExcludedGroups", @("")) $DefenderCommonSensitiveAccountFilter['SensitiveAccounts'].add("ExcludedDomains", @("")) foreach ($Filter in $CommonSensitiveAccountFilterNamespace){ $DefenderTemplate.add($Filter, $DefenderCommonSensitiveAccountFilter) } $DefenderTemplate.add($UserImpersonationProtectionNamespace, @{ SensitiveUsers = @("") }) $DefenderTemplate.add($AgencyDomainImpersonationProtectionNamespace, @{ AgencyDomains = @("") }) $DefenderTemplate.add($PartnerDomainImpersonationProtectionNamespace, @{ PartnerDomains = @("") }) $Products = (Get-Variable -Name ProductNames -EA SilentlyContinue).Value foreach ($Product in $Products){ switch ($Product){ "aad" { $config.add("Aad", $AadTemplate) } "defender" { $config.add("Defender", $DefenderTemplate) } } } convertto-yaml $Config | set-content "$($ConfigLocation)/SampleConfig.yaml" } Export-ModuleMember -Function @( 'Copy-SCuBABaselineDocument', 'Install-OPAforSCuBA', 'Initialize-SCuBA', 'Debug-SCuBA', 'Copy-SCuBASampleReport', 'Copy-SCuBASampleConfigFile', 'New-SCuBAConfig' ) # SIG # Begin signature block # MIIuuQYJKoZIhvcNAQcCoIIuqjCCLqYCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDCXoitI0oynqK5 # kKqb3ZqU5qxR8ro4oa2dUw2wI8o0m6CCE6MwggWQMIIDeKADAgECAhAFmxtXno4h # 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 # 9iGBoiRsS4U86S8xkDGCGmwwghpoAgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNV # BAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0 # IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENBMQIQDZEPHT770dKt # 2LbeB9E92jANBglghkgBZQMEAgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgACh # AoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAM # BgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCDgWQ/5NcEv4E305R61sKwBAhc1 # gAGYqrbKOpFA2uRvfTANBgkqhkiG9w0BAQEFAASCAgBj+RaoNi5BIV9swHoxIc6T # 89Vud8ASIZMtwh4SK4vfH9dix0RBeX2XfTT+3MhKeGjLE3O4wwp5zEsI48CK1q1H # KY8qZMWXxtx+9Go/nJ2WgIdamjbLCbAyjHB/Q3pgJDjW18YWML1P9b4mPKK3n4Hx # kxUIIV75n/PhvWHSNR+MZ/3EUiN5wpW2f/sxXGC8B2RvwxEGamsHySjh5JcuxKbV # fDqL2nuOPiHiJrwhZKEo4nNCpLXGmFzKXdEIhzY/A6nEiCZoAyxJrX3IsSzU4JWE # OdW81Ou7tc0cyGTHXx2IX+WVTpHP3TVnCq0jfAsNfHmNtmawB+w9sYn0CZXZ5prV # l/FmXwVY2x489tXdfOKe1OkXwHyvWgaqaA4HEqk1Y/1tORipt54WtfpxfeHobbPf # 0OCRLqEEI7WvDRRerugnfGY8n70+pfwAVvuOsMqlHXlhZFs1mSxHKVXMzrgz79g5 # ezaa63dVIDXAIJ8Bfn/fGcBLBoo5V4w24ZHbmuaceZ1nyJW+/CsqLdWXLddfuUMR # W8pz2lAt9QrzJIe/IP+38JFGmbegvaakM28qCCTa3itMf7IwrMgK+Z5TnlX3HLKb # pA837RsM5avjeJxsTASw3T2YKhAAJOwQwheenr29cUSQdgnRLC5GXmPEGloTdnas # 9q1f71jFa79y1m+115QEY6GCFzkwghc1BgorBgEEAYI3AwMBMYIXJTCCFyEGCSqG # SIb3DQEHAqCCFxIwghcOAgEDMQ8wDQYJYIZIAWUDBAIBBQAwdwYLKoZIhvcNAQkQ # AQSgaARmMGQCAQEGCWCGSAGG/WwHATAxMA0GCWCGSAFlAwQCAQUABCDtSJZiyTOi # 6F7sPz676hKJ4tYrvCCOA2cwO7R95TqDPAIQfZn84Iyx385kdLsQzNeVuxgPMjAy # NDEyMTkxODM2MzFaoIITAzCCBrwwggSkoAMCAQICEAuuZrxaun+Vh8b56QTjMwQw # DQYJKoZIhvcNAQELBQAwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0 # LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hB # MjU2IFRpbWVTdGFtcGluZyBDQTAeFw0yNDA5MjYwMDAwMDBaFw0zNTExMjUyMzU5 # NTlaMEIxCzAJBgNVBAYTAlVTMREwDwYDVQQKEwhEaWdpQ2VydDEgMB4GA1UEAxMX # RGlnaUNlcnQgVGltZXN0YW1wIDIwMjQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw # ggIKAoICAQC+anOf9pUhq5Ywultt5lmjtej9kR8YxIg7apnjpcH9CjAgQxK+CMR0 # Rne/i+utMeV5bUlYYSuuM4vQngvQepVHVzNLO9RDnEXvPghCaft0djvKKO+hDu6O # bS7rJcXa/UKvNminKQPTv/1+kBPgHGlP28mgmoCw/xi6FG9+Un1h4eN6zh926SxM # e6We2r1Z6VFZj75MU/HNmtsgtFjKfITLutLWUdAoWle+jYZ49+wxGE1/UXjWfISD # mHuI5e/6+NfQrxGFSKx+rDdNMsePW6FLrphfYtk/FLihp/feun0eV+pIF496OVh4 # R1TvjQYpAztJpVIfdNsEvxHofBf1BWkadc+Up0Th8EifkEEWdX4rA/FE1Q0rqViT # bLVZIqi6viEk3RIySho1XyHLIAOJfXG5PEppc3XYeBH7xa6VTZ3rOHNeiYnY+V4j # 1XbJ+Z9dI8ZhqcaDHOoj5KGg4YuiYx3eYm33aebsyF6eD9MF5IDbPgjvwmnAalNE # eJPvIeoGJXaeBQjIK13SlnzODdLtuThALhGtyconcVuPI8AaiCaiJnfdzUcb3dWn # qUnjXkRFwLtsVAxFvGqsxUA2Jq/WTjbnNjIUzIs3ITVC6VBKAOlb2u29Vwgfta8b # 2ypi6n2PzP0nVepsFk8nlcuWfyZLzBaZ0MucEdeBiXL+nUOGhCjl+QIDAQABo4IB # izCCAYcwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAww # CgYIKwYBBQUHAwgwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9bAcBMB8G # A1UdIwQYMBaAFLoW2W1NhS9zKXaaL3WMaiCPnshvMB0GA1UdDgQWBBSfVywDdw4o # FZBmpWNe7k+SH3agWzBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsMy5kaWdp # Y2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRSU0E0MDk2U0hBMjU2VGltZVN0YW1w # aW5nQ0EuY3JsMIGQBggrBgEFBQcBAQSBgzCBgDAkBggrBgEFBQcwAYYYaHR0cDov # L29jc3AuZGlnaWNlcnQuY29tMFgGCCsGAQUFBzAChkxodHRwOi8vY2FjZXJ0cy5k # aWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRSU0E0MDk2U0hBMjU2VGltZVN0 # YW1waW5nQ0EuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQA9rR4fdplb4ziEEkfZQ5H2 # EdubTggd0ShPz9Pce4FLJl6reNKLkZd5Y/vEIqFWKt4oKcKz7wZmXa5VgW9B76k9 # NJxUl4JlKwyjUkKhk3aYx7D8vi2mpU1tKlY71AYXB8wTLrQeh83pXnWwwsxc1Mt+ # FWqz57yFq6laICtKjPICYYf/qgxACHTvypGHrC8k1TqCeHk6u4I/VBQC9VK7iSpU # 5wlWjNlHlFFv/M93748YTeoXU/fFa9hWJQkuzG2+B7+bMDvmgF8VlJt1qQcl7YFU # MYgZU1WM6nyw23vT6QSgwX5Pq2m0xQ2V6FJHu8z4LXe/371k5QrN9FQBhLLISZi2 # yemW0P8ZZfx4zvSWzVXpAb9k4Hpvpi6bUe8iK6WonUSV6yPlMwerwJZP/Gtbu3CK # ldMnn+LmmRTkTXpFIEB06nXZrDwhCGED+8RsWQSIXZpuG4WLFQOhtloDRWGoCwwc # 6ZpPddOFkM2LlTbMcqFSzm4cd0boGhBq7vkqI1uHRz6Fq1IX7TaRQuR+0BGOzISk # cqwXu7nMpFu3mgrlgbAW+BzikRVQ3K2YHcGkiKjA4gi4OA/kz1YCsdhIBHXqBzR0 # /Zd2QwQ/l4Gxftt/8wY3grcc/nS//TVkej9nmUYu83BDtccHHXKibMs/yXHhDXNk # oPIdynhVAku7aRZOwqw6pDCCBq4wggSWoAMCAQICEAc2N7ckVHzYR6z9KGYqXlsw # DQYJKoZIhvcNAQELBQAwYjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0 # IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNl # cnQgVHJ1c3RlZCBSb290IEc0MB4XDTIyMDMyMzAwMDAwMFoXDTM3MDMyMjIzNTk1 # OVowYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYD # VQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hBMjU2IFRpbWVTdGFt # cGluZyBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMaGNQZJs8E9 # cklRVcclA8TykTepl1Gh1tKD0Z5Mom2gsMyD+Vr2EaFEFUJfpIjzaPp985yJC3+d # H54PMx9QEwsmc5Zt+FeoAn39Q7SE2hHxc7Gz7iuAhIoiGN/r2j3EF3+rGSs+Qtxn # jupRPfDWVtTnKC3r07G1decfBmWNlCnT2exp39mQh0YAe9tEQYncfGpXevA3eZ9d # rMvohGS0UvJ2R/dhgxndX7RUCyFobjchu0CsX7LeSn3O9TkSZ+8OpWNs5KbFHc02 # DVzV5huowWR0QKfAcsW6Th+xtVhNef7Xj3OTrCw54qVI1vCwMROpVymWJy71h6aP # TnYVVSZwmCZ/oBpHIEPjQ2OAe3VuJyWQmDo4EbP29p7mO1vsgd4iFNmCKseSv6De # 4z6ic/rnH1pslPJSlRErWHRAKKtzQ87fSqEcazjFKfPKqpZzQmiftkaznTqj1QPg # v/CiPMpC3BhIfxQ0z9JMq++bPf4OuGQq+nUoJEHtQr8FnGZJUlD0UfM2SU2LINIs # VzV5K6jzRWC8I41Y99xh3pP+OcD5sjClTNfpmEpYPtMDiP6zj9NeS3YSUZPJjAw7 # W4oiqMEmCPkUEBIDfV8ju2TjY+Cm4T72wnSyPx4JduyrXUZ14mCjWAkBKAAOhFTu # zuldyF4wEr1GnrXTdrnSDmuZDNIztM2xAgMBAAGjggFdMIIBWTASBgNVHRMBAf8E # CDAGAQH/AgEAMB0GA1UdDgQWBBS6FtltTYUvcyl2mi91jGogj57IbzAfBgNVHSME # GDAWgBTs1+OC0nFdZEzfLmc/57qYrhwPTzAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0l # BAwwCgYIKwYBBQUHAwgwdwYIKwYBBQUHAQEEazBpMCQGCCsGAQUFBzABhhhodHRw # Oi8vb2NzcC5kaWdpY2VydC5jb20wQQYIKwYBBQUHMAKGNWh0dHA6Ly9jYWNlcnRz # LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRSb290RzQuY3J0MEMGA1UdHwQ8 # MDowOKA2oDSGMmh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0 # ZWRSb290RzQuY3JsMCAGA1UdIAQZMBcwCAYGZ4EMAQQCMAsGCWCGSAGG/WwHATAN # BgkqhkiG9w0BAQsFAAOCAgEAfVmOwJO2b5ipRCIBfmbW2CFC4bAYLhBNE88wU86/ # GPvHUF3iSyn7cIoNqilp/GnBzx0H6T5gyNgL5Vxb122H+oQgJTQxZ822EpZvxFBM # Yh0MCIKoFr2pVs8Vc40BIiXOlWk/R3f7cnQU1/+rT4osequFzUNf7WC2qk+RZp4s # nuCKrOX9jLxkJodskr2dfNBwCnzvqLx1T7pa96kQsl3p/yhUifDVinF2ZdrM8HKj # I/rAJ4JErpknG6skHibBt94q6/aesXmZgaNWhqsKRcnfxI2g55j7+6adcq/Ex8HB # anHZxhOACcS2n82HhyS7T6NJuXdmkfFynOlLAlKnN36TU6w7HQhJD5TNOXrd/yVj # mScsPT9rp/Fmw0HNT7ZAmyEhQNC3EyTN3B14OuSereU0cZLXJmvkOHOrpgFPvT87 # eK1MrfvElXvtCl8zOYdBeHo46Zzh3SP9HSjTx/no8Zhf+yvYfvJGnXUsHicsJttv # FXseGYs2uJPU5vIXmVnKcPA3v5gA3yAWTyf7YGcWoWa63VXAOimGsJigK+2VQbc6 # 1RWYMbRiCQ8KvYHZE/6/pNHzV9m8BPqC3jLfBInwAM1dwvnQI38AC+R2AibZ8GV2 # QqYphwlHK+Z/GqSFD/yYlvZVVCsfgPrA8g4r5db7qS9EFUrnEw4d2zc4GqEr9u3W # fPwwggWNMIIEdaADAgECAhAOmxiO+dAt5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUA # MGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsT # EHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQg # Um9vdCBDQTAeFw0yMjA4MDEwMDAwMDBaFw0zMTExMDkyMzU5NTlaMGIxCzAJBgNV # BAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdp # Y2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIw # DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL/mkHNo3rvkXUo8MCIwaTPswqcl # LskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/zG6Q4FutWxpdtHauyefLKEdLkX9YF # PFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZanMylNEQRBAu34LzB4TmdDttceIt # DBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7sWxq868nPzaw0QF+xembud8hIqGZX # V59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1 # ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfbBHMqbpEBfCFM1LyuGwN1XXhm2Tox # RJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3JFxGj2T3wWmIdph2PVldQnaHiZdp # ekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF # 30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqxYxhElRp2Yn72gLD76GSmM9GJB+G9 # t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0viastkF13nqsX40/ybzTQRESW+UQ # UOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aLT8LWRV+dIPyhHsXAj6KxfgommfXk # aS+YHS312amyHeUbAgMBAAGjggE6MIIBNjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud # DgQWBBTs1+OC0nFdZEzfLmc/57qYrhwPTzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEt # UYunpyGd823IDzAOBgNVHQ8BAf8EBAMCAYYweQYIKwYBBQUHAQEEbTBrMCQGCCsG # AQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0 # dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RD # QS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29t # L0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDARBgNVHSAECjAIMAYGBFUdIAAw # DQYJKoZIhvcNAQEMBQADggEBAHCgv0NcVec4X6CjdBs9thbX979XB72arKGHLOyF # XqkauyL4hxppVCLtpIh3bb0aFPQTSnovLbc47/T/gLn4offyct4kvFIDyE7QKt76 # LVbP+fT3rDB6mouyXtTP0UNEm0Mh65ZyoUi0mcudT6cGAxN3J0TU53/oWajwvy8L # punyNDzs9wPHh6jSTEAZNUZqaVSwuKFWjuyk1T3osdz9HNj0d1pcVIxv76FQPfx2 # CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPFmCLBsln1VWvPJ6tsds5vIy30fnFqI2si # /xK4VC0nftg62fC2h5b9W9FcrBjDTZ9ztwGpn1eqXijiuZQxggN2MIIDcgIBATB3 # MGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UE # AxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBp # bmcgQ0ECEAuuZrxaun+Vh8b56QTjMwQwDQYJYIZIAWUDBAIBBQCggdEwGgYJKoZI # hvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEyMTkxODM2 # MzFaMCsGCyqGSIb3DQEJEAIMMRwwGjAYMBYEFNvThe5i29I+e+T2cUhQhyTVhltF # MC8GCSqGSIb3DQEJBDEiBCCy56HW5M4KY0e4lKsCoOQtEFhU/4mE3eX6MpP+ltt4 # gjA3BgsqhkiG9w0BCRACLzEoMCYwJDAiBCB2dp+o8mMvH0MLOiMwrtZWdf7Xc9sF # 1mW5BZOYQ4+a2zANBgkqhkiG9w0BAQEFAASCAgCSlXZSGxo3BNYEj8GdLESmBe6v # c1Ab3eM+bn8Q0XCEiPDjLRhs7Gqtnajb+DPcUYhSm7dL1oPNY5HgrDCTCxlNlE13 # q5Q4oF06uvqlQ/PL4R8qzDFomLYrQ+QQ6Z4cVZc2sOJ/Y1WcJWVMHJi01a/Wyn/v # ysFsTRNk3hdAr5sCjsuC428lzsGMWQ1Sm2LnmZNN6NjutlB8AvlJwOFi6AVGuOGe # Qy+8WsAsP7Ls2Za1rKd/O54NXk9d6Tw9bijCup7JUuDIL+ZWzDxntV26P0nIoVVj # lSKkHSVfSFjaHBxUgyOWWIHbQT33DF3zQsviPym5RSyJhb2VexATuUc8TQgKn4HH # trgPvDo7ai9+o5bmCjLOzSnUaSDZ7LjT/ljb2jYHdWQQERjd/R2MgSRlwXnddDkq # K/7JQB6dAiEbDIhwJlo/rduC30PHfYZA58DsFPEycJOi+eRCeKnjS+/Tnk6aXobB # K0lgbDNqiTyM/8owAHCw12E+tCD7UcFbsmlFVon4EchsLfh7jgUAqY+M++5JLLqj # k360bg5WrgN7ryZ84ovscIjx0pD4hndWH5KcWsgssEox8VWRgIVKzQHZl75ih48S # FyxkF1AuP91WOEllWglEQxyLCykGS5MwmwGmvrdQdzShcSP3aSrPM378d1MINUBM # jfH6EyERg8qFo6Q8yQ== # SIG # End signature block |