CAMP.psm1
#Requires -Version 5.1 <# .SYNOPSIS CAMP - Configuration Analyzer for Microsoft Purview (CAMP) .DESCRIPTION .NOTES Neha Pandey Senior Software Engineer - Microsoft Kritika Mishra Software Engineer - Microsoft Output report uses open source components for HTML formatting - bootstrap - MIT License - https://getbootstrap.com/docs/4.0/about/license/ - fontawesome - CC BY 4.0 License - https://fontawesome.com/license/free ############################################################################ This sample script is not supported under any Microsoft standard support program or service. This sample script is provided AS IS without warranty of any kind. Microsoft further disclaims all implied warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose. The entire risk arising out of the use or performance of the sample script and documentation remains with you. In no event shall Microsoft, its authors, or anyone else involved in the creation, production, or delivery of the scripts be liable for any damages whatsoever (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability to use the sample script or documentation, even if Microsoft has been advised of the possibility of such damages. ############################################################################ .LINK about_functions_advanced #> [bool] $global:ErrorOccurred = $false # TelemetryEnabled [bool] $global:TelemetryEnabled = $false # Connection Established [bool] $global:ConnectionEstablished = $false [string] $global:EnvironmentName = "" [string] $global:UserName = "" function Get-CAMPDirectory { <# Gets or creates the CAMP directory in AppData #> If ($IsWindows) { $Directory = "$($env:LOCALAPPDATA)\Microsoft\CAMP" } elseif ($IsLinux -or $IsMac) { $Directory = "$($env:HOME)/CAMP" } else { $Directory = "$($env:LOCALAPPDATA)\Microsoft\CAMP" } If (Test-Path $Directory) { Return $Directory } else { mkdir $Directory | out-null Return $Directory } } Function Invoke-CAMPConnections { Param ( [String]$ExchangeEnvironmentName, [String]$LogFile ) try { try { $ExchangeVersion = (Get-InstalledModule -name "ExchangeOnlineManagement" -ErrorAction:SilentlyContinue | Sort-Object Version -Desc)[0].Version } catch { $ExchangeVersion = "Error" write-host "$(Get-Date) Exchange Online Management module is not installed. Installing.." Write-Verbose "Installing ExchangeOnlineManagement" Install-Module -Name "ExchangeOnlineManagement" -force } if ($ExchangeVersion -eq "Error") { $ExchangeVersion = (Get-InstalledModule -name "ExchangeOnlineManagement" -ErrorAction:SilentlyContinue | Sort-Object Version -Desc)[0].Version } if ("$ExchangeVersion" -lt "2.0.3") { write-host "$(Get-Date) Your Exchange Online Management module is not updated. Updating.." Update-Module -Name "ExchangeOnlineManagement" -RequiredVersion 2.0.3 } $global:ConnectionEstablished = $true $userName = Read-Host -Prompt 'Input the user name' -ErrorAction:SilentlyContinue $global:UserName = $userName $InfoMessage = "Connecting to Exchange Online (Modern Module).." Write-Host "$(Get-Date) $InfoMessage" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue Connect-ExchangeOnline -Prefix EXOP -UserPrincipalName $userName -ExchangeEnvironmentName $ExchangeEnvironmentName -ShowBanner:$false -ErrorAction:SilentlyContinue -WarningAction:SilentlyContinue } catch { Write-Host "Error:$(Get-Date) There was an issue in connecting to Exchange Online. Please try running the tool again after some time." -ForegroundColor:Red $ErrorMessage = $_.ToString() $StackTraceInfo = $_.ScriptStackTrace Write-Log -IsError -ErrorMessage $ErrorMessage -StackTraceInfo $StackTraceInfo -LogFile $LogFile -ErrorAction:SilentlyContinue } try { switch ($ExchangeEnvironmentName) { #O365China { } #O365GermanyCloud { $ConnectionUri = 'https://ps.compliance.protection.outlook.de/' } O365USGovDoD { $ConnectionUri = 'https://l5.ps.compliance.protection.office365.us/powershell-liveid/' } O365USGovGCCHigh { $ConnectionUri = 'https://ps.compliance.protection.office365.us/powershell-liveid/' } Default { $ConnectionUri = '' } } $InfoMessage = "Connecting to Security & Compliance Center" Write-Host "$(Get-Date) $InfoMessage" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue if ($ConnectionUri -eq '') { Connect-IPPSSession -UserPrincipalName $userName -ErrorAction:SilentlyContinue -WarningAction:SilentlyContinue } else { Connect-IPPSSession -UserPrincipalName $userName -ConnectionUri $ConnectionUri -ErrorAction:SilentlyContinue -WarningAction:SilentlyContinue } try { $statusCode = wget http://aka.ms/mcca-execution -Method head | % { $_.StatusCode } }catch {} } catch { Write-Host "Error:$(Get-Date) There was an issue in connecting to Security & Compliance Center. Please try running the tool again after some time." -ForegroundColor:Red $ErrorMessage = $_.ToString() $StackTraceInfo = $_.ScriptStackTrace Write-Log -IsError -ErrorMessage $ErrorMessage -StackTraceInfo $StackTraceInfo -LogFile $LogFile -ErrorAction:SilentlyContinue throw 'There was an issue in connecting to Security & Compliance Center. Please try running the tool again after some time.' } } enum CheckType { ObjectPropertyValue PropertyValue } [Flags()] enum CAMPService { DLP = 1 OATP = 2 } enum CAMPConfigLevel { None = 0 Recommendation = 4 Ok = 5 Informational = 10 TooStrict = 15 } enum CAMPResult { Pass = 1 Recommendation = 2 Fail = 3 } Class CAMPCheckConfig { CAMPCheckConfig() { # Constructor $this.Results += New-Object -TypeName CAMPCheckConfigResult -Property @{ Level = [CAMPConfigLevel]::Recommendation } $this.Results += New-Object -TypeName CAMPCheckConfigResult -Property @{ Level = [CAMPConfigLevel]::Ok } $this.Results += New-Object -TypeName CAMPCheckConfigResult -Property @{ Level = [CAMPConfigLevel]::Informational } $this.Results += New-Object -TypeName CAMPCheckConfigResult -Property @{ Level = [CAMPConfigLevel]::TooStrict } } # Set the result for this mode SetResult([CAMPConfigLevel]$Level, $Result) { ($this.Results | Where-Object { $_.Level -eq $Level }).Value = $Result # The level of this configuration should be its strongest result (e.g if its currently Ok and we have a Informational pass, we should make the level Informational) if ($Result -eq "Pass" -and ($this.Level -lt $Level -or $this.Level -eq [CAMPConfigLevel]::None)) { $this.Level = $Level } elseif ($Result -eq "Fail" -and ($Level -eq [CAMPConfigLevel]::Recommendation -and $this.Level -eq [CAMPConfigLevel]::None)) { $this.Level = $Level } } $Check $Object $ConfigItem $ConfigData $InfoText [string]$RemediationAction = "" [array]$Results [CAMPConfigLevel]$Level } Class CAMPCheckConfigResult { [CAMPConfigLevel]$Level = [CAMPConfigLevel]::Ok $Value } Class CAMPRemediationInfo { [bool]$RemediationAvailable = $false [string]$RemediationText = "" } Class CAMPCheck { <# Check definition The checks defined below allow contextual information to be added in to the report HTML document. - Control : A unique identifier that can be used to index the results back to the check - Area : The area that this check should appear within the report - PassText : The text that should appear in the report when this 'control' passes - FailRecommendation : The text that appears as a title when the 'control' fails. Short, descriptive. E.g "Do this" - Importance : Why this is important - ExpandResults : If we should create a table in the callout which points out which items fail and where - ObjectType : When ExpandResults is set to, For Object, Property Value checks - what is the name of the Object, e.g a Spam Policy - ItemName : When ExpandResults is set to, what does the check return as ConfigItem, for instance, is it a Transport Rule? - DataType : When ExpandResults is set to, what type of data is returned in ConfigData, for instance, is it a Domain? #> [Array] $Config = @() [string] $Control [string] $ParentArea [String] $Area [String] $Name [String] $PassText [String] $FailRecommendation [Boolean] $ExpandResults = $false [String] $ObjectType [String] $ItemName [String] $DataType [String] $Importance [CAMPService]$Services = [CAMPService]::DLP [CheckType] $CheckType = [CheckType]::PropertyValue [CAMPRemediationInfo] $CAMPRemediationInfo [string] $LogFile [string] $ExchangeEnvironmentNameForCheck = $global:EnvironmentName $Links $CAMPParams [CAMPResult] $Result = [CAMPResult]::Pass [int] $FailCount = 0 [int] $PassCount = 0 [int] $InfoCount = 0 [Boolean] $Completed = $false # Overridden by check GetResults($Config) { } AddConfig([CAMPCheckConfig]$Config) { $this.Config += $Config $this.FailCount = @($this.Config | Where-Object { $_.Level -eq [CAMPConfigLevel]::None }).Count $this.PassCount = @($this.Config | Where-Object { $_.Level -eq [CAMPConfigLevel]::Ok -or $_.Level -eq [CAMPConfigLevel]::Informational }).Count $this.InfoCount = @($this.Config | Where-Object { $_.Level -eq [CAMPConfigLevel]::Recommendation }).Count If ($this.FailCount -eq 0 -and $this.InfoCount -eq 0) { $this.Result = [CAMPResult]::Pass } elseif ($this.FailCount -eq 0 -and $this.InfoCount -gt 0) { $this.Result = [CAMPResult]::Recommendation } else { $this.Result = [CAMPResult]::Fail } } # Run Run($Config) { Write-Host "$(Get-Date) Analysis - $($this.Area) - $($this.Name)" $this.GetResults($Config) # If there is no results to expand, turn off ExpandResults if ($this.Config.Count -eq 0) { $this.ExpandResults = $false } } } Class CAMPOutput { [String] $Name [Boolean] $Completed = $False $VersionCheck $DefaultOutputDirectory $Result # Function overridden RunOutput($Checks, $Collection) { } Run($Checks, $Collection) { $this.RunOutput($Checks, $Collection) $this.Completed = $True } } Class RemediationAction { [String] $Name [Boolean] $Completed = $False $VersionCheck $DefaultOutputDirectory $Result # Function overridden RunOutput($Checks, $Collection) { } Run($Checks, $Collection) { $this.RunOutput($Checks, $Collection) $this.Completed = $True } } Function Get-CAMPCheckDefs { Param ( [string]$LogFile, $CAMPParams, $Collection ) $Checks = @() # Load individual check definitions $CheckFiles = Get-ChildItem "$PSScriptRoot\Checks" # DLP check file full name $DLPCheckFileName = $null #Setting DLP check file name ForEach ($CheckFile in $CheckFiles) { if (($CheckFile.BaseName -match '^check-(.*)$') -and ($matches[1] -like "DLP")) { $DLPCheckFileName = $CheckFile.FullName } } #Creating DLP check objects for each improvement actions #read xml doc if ($($Collection["GetRequiredSolution"]) -icontains "DLP") { [xml]$CheckData = Get-Content "$PSScriptRoot\DLPImprovementActions\ActionsInformation.xml" if ($null -eq $CheckData -or $CheckData -eq "") { Write-Host "$(Get-Date) ActionsInformation.xml file does not exist/is corrupt in $PSScriptRoot\DLPImprovementActions\ActionsInformation.xml." -ForegroundColor Orange } if ($null -ne $DLPCheckFileName -or $DLPCheckFileName -ne "") { Write-Verbose "Importing DLP" . $DLPCheckFileName foreach ($Item in $CheckData.ImprovementActions.ActionItem) { #List of SIT $ListOfSIT = @() $AllSITS = $Item.SITs.SIT #Adding custom SITS <# if($($Collection["GetDLPCustomSIT"]) -ne "Error") { $CustomSIT = $($Collection["GetDLPCustomSIT"]).Name foreach ($sit in $CustomSIT) { $ListOfSIT += $sit } } #> if ($($Collection["GetOrganisationRegion"]) -eq "Error") { foreach ($sit in $AllSITS) { $ListOfSIT += $sit.InnerText } } else { foreach ($sit in $AllSITS) { if ($($Collection["GetOrganisationRegion"]) -contains $($sit.Geo)) { $ListOfSIT += $sit.InnerText } } } #Hash table of links $LinksInfo = @{} if ($global:EnvironmentName -ieq "O365USGovGCCHigh") { $AllLinks = $Item.GCCLinks.Link } elseif ($global:EnvironmentName -ieq "O365USGovDoD") { $AllLinks = $Item.DODLinks.Link } else { $AllLinks = $Item.Links.Link } foreach ($url in $AllLinks) { $LinksInfo[$url.LinkText] = $url.ActualURL } $InfoParams = @{} $InfoParams["Control"] = $Item.CheckName $InfoParams["ParentArea"] = $Item.ParentArea $InfoParams["Area"] = $Item.Area $InfoParams["Name"] = $Item.Name $InfoParams["RemediationPolicyName"] = $Item.RemediationPolicyName $InfoParams["PassText"] = $Item.PassText $InfoParams["FailRecommendation"] = $Item.FailRecommendation $InfoParams["Importance"] = $Item.Importance $InfoParams["SIT"] = $ListOfSIT $InfoParams["Links"] = $LinksInfo $Check = New-Object -TypeName "DLP" -ArgumentList $InfoParams # Set the CAMPParams $Check.CAMPParams = $CAMPParams $Check.LogFile = $LogFile $Checks += $Check } } } # Creating Non-DLP check objects for each improvement actions ForEach ($CheckFile in $CheckFiles) { if ($CheckFile.BaseName -match '^check-(.*)$' -and ($matches[1] -notlike "DLP")) { $solutioname = $matches[1] $length = $solutioname.length $solutioname = $solutioname.substring(0, $length - 3) if (($null -ne $($Collection["GetRequiredSolution"])) -and ($($Collection["GetRequiredSolution"]) -icontains "$solutioname")) { Write-Verbose "Importing $($matches[1])" . $CheckFile.FullName $Check = New-Object -TypeName $matches[1] # Set the CAMPParams $Check.CAMPParams = $CAMPParams $Check.LogFile = $LogFile $Checks += $Check } } } ForEach ($CheckFile in $CheckFiles) { if ($CheckFile.BaseName -match '^check-(.*)$' -and ($matches[1] -like "ComplianceManager")) { #write-host "abc" Write-Verbose "Importing $($matches[1])" . $CheckFile.FullName $Check = New-Object -TypeName $matches[1] # Set the CAMPParams $Check.CAMPParams = $CAMPParams $Check.LogFile = $LogFile $Checks += $Check } } $Checks = $Checks | Sort-Object -Property @{ expression = 'ParentArea' ; descending = $true }, @{expression = 'Area' ; descending = $false } Return $Checks } Function Get-CAMPRemediationAction { Param ( $VersionCheck ) $RemediationActions = @() # Load individual check definitions $RemediationActionOutputFiles = Get-ChildItem "$PSScriptRoot\Remediation" ForEach ($RemediationActionOutputFile in $RemediationActionOutputFiles) { if ($RemediationActionOutputFile.BaseName -match '^remediation(.*)$') { Write-Verbose "Importing $($matches[1])" . $RemediationActionOutputFile.FullName $RemediationAction = New-Object -TypeName $matches[1] # For default output directory $RemediationAction.DefaultOutputDirectory = Get-CAMPDirectory # Provide versioncheck $RemediationAction.VersionCheck = $VersionCheck $RemediationActions += $RemediationAction } } Return $RemediationActions } Function Get-CAMPOutputs { Param ( $VersionCheck, $Modules, $Options ) $Outputs = @() # Load individual check definitions $OutputFiles = Get-ChildItem "$PSScriptRoot\Outputs" ForEach ($OutputFile in $OutputFiles) { if ($OutputFile.BaseName -match '^output-(.*)$') { # Determine if this type should be loaded If ($Modules -contains $matches[1]) { Write-Verbose "Importing $($matches[1])" . $OutputFile.FullName $Output = New-Object -TypeName $matches[1] # Load any of the options in to the module If ($Options) { If ($Options[$matches[1]].Keys) { ForEach ($Opt in $Options[$matches[1]].Keys) { # Ensure this property exists before we try set it and get a null ref error $ModProperties = $($Output | Get-Member | Where-Object { $_.MemberType -eq "Property" }).Name If ($ModProperties -contains $Opt) { $Output.$Opt = $Options[$matches[1]][$Opt] } else { Throw("There is no option $($Opt) on output module $($matches[1])") } } } } # For default output directory $Output.DefaultOutputDirectory = Get-CAMPDirectory # Provide versioncheck $Output.VersionCheck = $VersionCheck $Outputs += $Output } } } Return $Outputs } # Get DLP settings Function Get-DataLossPreventionSettings { Param( $Collection, [string]$LogFile ) try { [System.Collections.ArrayList]$WarnMessage = @() $Collection["GetDlpComplianceRule"] = Get-DlpComplianceRule -ErrorAction:SilentlyContinue -WarningVariable +WarnMessage $Collection["GetDLPCustomSIT"] = Get-DlpSensitiveInformationType -ErrorAction:SilentlyContinue -WarningVariable +WarnMessage | Where-Object { $_.Publisher -ne "Microsoft Corporation" } $Collection["GetDlpCompliancePolicy"] = Get-DlpCompliancePolicy -ErrorAction:SilentlyContinue -WarningVariable +WarnMessage Write-Log -IsWarn -WarnMessage $WarnMessage -LogFile $LogFile -ErrorAction:SilentlyContinue } catch { $Collection["GetDlpComplianceRule"] = "Error" $Collection["GetDLPCustomSIT"] = "Error" $Collection["GetDlpCompliancePolicy"] = "Error" Write-Host "Error:$(Get-Date) There was an issue in fetching Data Loss Prevention information. Please try running the tool again after some time." -ForegroundColor:Red $ErrorMessage = $_.ToString() $StackTraceInfo = $_.ScriptStackTrace Write-Log -IsError -ErrorMessage $ErrorMessage -StackTraceInfo $StackTraceInfo -LogFile $LogFile -ErrorAction:SilentlyContinue } Return $Collection } # Get Information Protection settings Function Get-InformationProtectionSettings { Param( $Collection, [string]$LogFile ) try { [System.Collections.ArrayList]$WarnMessage = @() $Collection["GetLabel"] = Get-Label -ErrorAction:SilentlyContinue -WarningVariable +WarnMessage try { $Collection["GetLabelPolicy"] = Get-LabelPolicy -ErrorAction:SilentlyContinue -WarningVariable +WarnMessage } catch { $Collection["GetLabelPolicy"] = "Error" } Write-Log -IsWarn -WarnMessage $WarnMessage -LogFile $LogFile -ErrorAction:SilentlyContinue } catch { $Collection["GetLabel"] = "Error" $Collection["GetLabelPolicy"] = "Error" Write-Host "Error:$(Get-Date) There was an issue in fetching Information Protection information. Please try running the tool again after some time." -ForegroundColor:Red $ErrorMessage = $_.ToString() $StackTraceInfo = $_.ScriptStackTrace Write-Log -IsError -ErrorMessage $ErrorMessage -StackTraceInfo $StackTraceInfo -LogFile $LogFile -ErrorAction:SilentlyContinue } try { [System.Collections.ArrayList]$WarnMessage = @() $Collection["GetAutoSensitivityLabelPolicy"] = Get-AutoSensitivityLabelPolicy -ErrorAction:SilentlyContinue -WarningVariable +WarnMessage Write-Log -IsWarn -WarnMessage $WarnMessage -LogFile $LogFile -ErrorAction:SilentlyContinue } catch { $Collection["GetAutoSensitivityLabelPolicy"] = "Error" Write-Host "Error:$(Get-Date) There was an issue in fetching AutoSensitivity Label Policy information. Please try running the tool again after some time." -ForegroundColor:Red $ErrorMessage = $_.ToString() $StackTraceInfo = $_.ScriptStackTrace Write-Log -IsError -ErrorMessage $ErrorMessage -StackTraceInfo $StackTraceInfo -LogFile $LogFile -ErrorAction:SilentlyContinue } try { [System.Collections.ArrayList]$WarnMessage = @() $Collection["GetIRMConfiguration"] = Get-EXOPIRMConfiguration -ErrorAction:SilentlyContinue -WarningVariable +WarnMessage Write-Log -IsWarn -WarnMessage $WarnMessage -LogFile $LogFile -ErrorAction:SilentlyContinue } catch { $Collection["GetIRMConfiguration"] = "Error" Write-Host "Error:$(Get-Date) There was an issue in fetching IRM Configuration information. Please try running the tool again after some time." -ForegroundColor:Red $ErrorMessage = $_.ToString() $StackTraceInfo = $_.ScriptStackTrace Write-Log -IsError -ErrorMessage $ErrorMessage -StackTraceInfo $StackTraceInfo -LogFile $LogFile -ErrorAction:SilentlyContinue } Return $Collection } # Get Communication Compliance settings Function Get-CommunicationComplianceSettings { Param( $Collection, [string]$LogFile ) try { [System.Collections.ArrayList]$WarnMessage = @() $Collection["GetSupervisoryReviewPolicyV2"] = Get-SupervisoryReviewPolicyV2 -ErrorAction:SilentlyContinue -WarningVariable +WarnMessage try { $Collection["GetSupervisoryReviewOverallProgressReport"] = Get-SupervisoryReviewOverallProgressReport -ErrorAction:SilentlyContinue -WarningVariable +WarnMessage } catch { $Collection["GetSupervisoryReviewOverallProgressReport"] = "Error" } Write-Log -IsWarn -WarnMessage $WarnMessage -LogFile $LogFile -ErrorAction:SilentlyContinue } catch { $Collection["GetSupervisoryReviewPolicyV2"] = "Error" $Collection["GetSupervisoryReviewOverallProgressReport"] = "Error" Write-Host "Error:$(Get-Date) There was an issue in fetching Communication Compliance information. Please try running the tool again after some time." -ForegroundColor:Red $ErrorMessage = $_.ToString() $StackTraceInfo = $_.ScriptStackTrace Write-Log -IsError -ErrorMessage $ErrorMessage -StackTraceInfo $StackTraceInfo -LogFile $LogFile -ErrorAction:SilentlyContinue } Return $Collection } # Get Information Governance settings Function Get-InformationGovernanceSettings { Param( $Collection, [string]$LogFile ) try { [System.Collections.ArrayList]$WarnMessage = @() $Collection["GetRetentionCompliancePolicy"] = Get-RetentionCompliancePolicy -DistributionDetail -ErrorAction:SilentlyContinue -WarningVariable +WarnMessage $Collection["GetRetentionComplianceRule"] = Get-RetentionComplianceRule -ErrorAction:SilentlyContinue -WarningVariable +WarnMessage $Collection["GetComplianceTag"] = Get-ComplianceTag -ErrorAction:SilentlyContinue -WarningVariable +WarnMessage Write-Log -IsWarn -WarnMessage $WarnMessage -LogFile $LogFile -ErrorAction:SilentlyContinue } catch { $Collection["GetRetentionCompliancePolicy"] = "Error" $Collection["GetRetentionComplianceRule"] = "Error" $Collection["GetComplianceTag"] = "Error" Write-Host "Error:$(Get-Date) There was an issue in fetching Retention Compliance information. Please try running the tool again after some time." -ForegroundColor:Red $ErrorMessage = $_.ToString() $StackTraceInfo = $_.ScriptStackTrace Write-Log -IsError -ErrorMessage $ErrorMessage -StackTraceInfo $StackTraceInfo -LogFile $LogFile -ErrorAction:SilentlyContinue } Return $Collection } # Get Audit settings Function Get-AuditSettings { Param( $Collection, [string]$LogFile ) try { [System.Collections.ArrayList]$WarnMessage = @() $Collection["GetAdminAuditLogConfig"] = Get-EXOPAdminAuditLogConfig -ErrorAction:SilentlyContinue -WarningVariable +WarnMessage Write-Log -IsWarn -WarnMessage $WarnMessage -LogFile $LogFile -ErrorAction:SilentlyContinue } catch { $Collection["GetAdminAuditLogConfig"] = "Error" Write-Host "Error:$(Get-Date) There was an issue in fetching Audit Configuration information. Please try running the tool again after some time." -ForegroundColor:Red $ErrorMessage = $_.ToString() $StackTraceInfo = $_.ScriptStackTrace Write-Log -IsError -ErrorMessage $ErrorMessage -StackTraceInfo $StackTraceInfo -LogFile $LogFile -ErrorAction:SilentlyContinue } Return $Collection } #get eDiscovery Function Get-eDiscoverySettings { Param( $Collection, [string]$LogFile ) try { [System.Collections.ArrayList]$WarnMessage = @() $Collection["GetComplianceCase"] = Get-ComplianceCase -CaseType AdvancedEdiscovery -ErrorAction:SilentlyContinue -WarningVariable +WarnMessage $Collection["GetComplianceCaseCore"] = Get-ComplianceCase -ErrorAction:SilentlyContinue -WarningVariable +WarnMessage Write-Log -IsWarn -WarnMessage $WarnMessage -LogFile $LogFile -ErrorAction:SilentlyContinue } catch { $Collection["GetComplianceCase"] = "Error" $Collection["GetComplianceCaseCore"] = "Error" Write-Host "Error:$(Get-Date) There was an issue in fetching Audit Configuration information. Please try running the tool again after some time." -ForegroundColor:Red $ErrorMessage = $_.ToString() $StackTraceInfo = $_.ScriptStackTrace Write-Log -IsError -ErrorMessage $ErrorMessage -StackTraceInfo $StackTraceInfo -LogFile $LogFile -ErrorAction:SilentlyContinue } Return $Collection } #Get Insider Risk Management Settings Function Get-InsiderRiskManagementSettings { Param( $Collection, [string]$LogFile ) try { [System.Collections.ArrayList]$WarnMessage = @() $Collection["GetInsiderRiskPolicy"] = Get-InsiderRiskPolicy -ErrorAction:SilentlyContinue -WarningVariable +WarnMessage Write-Log -IsWarn -WarnMessage $WarnMessage -LogFile $LogFile -ErrorAction:SilentlyContinue } catch { $Collection["GetInsiderRiskPolicy"] = "Error" Write-Host "Error:$(Get-Date) There was an issue in fetching Insider Risk Management information. Please try running the tool again after some time." -ForegroundColor:Red $ErrorMessage = $_.ToString() $StackTraceInfo = $_.ScriptStackTrace Write-Log -IsError -ErrorMessage $ErrorMessage -StackTraceInfo $StackTraceInfo -LogFile $LogFile -ErrorAction:SilentlyContinue } Return $Collection } # Get Accepted Domains Function Get-AcceptedDomains { Param( $Collection, [string]$LogFile ) try { [System.Collections.ArrayList]$WarnMessage = @() $Collection["AcceptedDomains"] = Get-EXOPAcceptedDomain -ErrorAction:SilentlyContinue -WarningVariable +WarnMessage Write-Log -IsWarn -WarnMessage $WarnMessage -LogFile $LogFile -ErrorAction:SilentlyContinue } catch { $Collection["AcceptedDomains"] = "Error" Write-Host "Error:$(Get-Date) There was an issue in fetching tenant name information. Please try running the tool again after some time." -ForegroundColor:Red $ErrorMessage = $_.ToString() $StackTraceInfo = $_.ScriptStackTrace Write-Log -IsError -ErrorMessage $ErrorMessage -StackTraceInfo $StackTraceInfo -LogFile $LogFile -ErrorAction:SilentlyContinue } Return $Collection } #Get Alert Policies Function Get-AlertPolicies { Param( $Collection, [string]$LogFile ) try { [System.Collections.ArrayList]$WarnMessage = @() $Collection["GetProtectionAlert"] = Get-ProtectionAlert | Where-Object { $_.Severity -eq "High" } -ErrorAction:SilentlyContinue -WarningVariable +WarnMessage Write-Log -IsWarn -WarnMessage $WarnMessage -LogFile $LogFile -ErrorAction:SilentlyContinue } catch { $Collection["GetProtectionAlert"] = "Error" Write-Host "Error:$(Get-Date) There was an issue in fetching Alert Policies Configuration information. Please try running the tool again after some time." -ForegroundColor:Red $ErrorMessage = $_.ToString() $StackTraceInfo = $_.ScriptStackTrace Write-Log -IsError -ErrorMessage $ErrorMessage -StackTraceInfo $StackTraceInfo -LogFile $LogFile -ErrorAction:SilentlyContinue } Return $Collection } #Get Organisation Region Function Get-OrganisationRegion { Param( $Collection, [string]$LogFile, [System.Collections.ArrayList] $GeoList ) try { [System.Collections.ArrayList]$WarnMessage = @() [System.Collections.ArrayList] $RegionNamesList = @() $Collection["GetOrganisationConfig"] = Get-EXOPOrganizationConfig -ErrorAction:SilentlyContinue if ($($GeoList.Count) -gt 0) { $Collection["GetOrganisationRegion"] = $GeoList $Collection["GetOrganisationRegion"].add("INTL") | out-null } else { $RegionsList = $Collection["GetOrganisationConfig"].AllowedMailboxRegions foreach ($region in $RegionsList) { $RegionName = $($region.Split("="))[0] $RegionName = $RegionName.ToUpper() $RegionNamesList.add($RegionName) | Out-Null } $Collection["GetOrganisationRegion"] = $RegionNamesList $Collection["GetOrganisationRegion"].add("INTL") | out-null } Write-Log -IsWarn -WarnMessage $WarnMessage -LogFile $LogFile -ErrorAction:SilentlyContinue } catch { $Collection["GetOrganisationConfig"] = "Error" if ($($GeoList.Count) -gt 0) { $Collection["GetOrganisationRegion"] = $GeoList $Collection["GetOrganisationRegion"].add("INTL") | out-null } else { $Collection["GetOrganisationRegion"] = "Error" Write-Host "Warning:$(Get-Date) There was an issue in fetching your tenant's geolocation. The generated report will have recommendations for all geos across the globe." -ForegroundColor:Yellow } $ErrorMessage = $_.ToString() $StackTraceInfo = $_.ScriptStackTrace Write-Log -IsError -ErrorMessage $ErrorMessage -StackTraceInfo $StackTraceInfo -LogFile $LogFile -ErrorAction:SilentlyContinue } Return $Collection } #Get Solution Config Function Get-PersonalizedSolution { Param( $Collection, [string]$LogFile, [System.Collections.ArrayList] $SolutionList ) [System.Collections.ArrayList] $SolutionsList = @() if ($($SolutionList.Count) -gt 0) { $Collection["GetRequiredSolution"] = $SolutionList $Collection["GetRequiredSolution"].add("INTL") | out-null } else { $SolutionTable = Get-SolutionTable [int] $count = 1 while ($count -le 8) { $SolutionList.add($($($SolutionTable[$count]).Code)) | out-null $count = $count + 1 } $Collection["GetRequiredSolution"] = $SolutionsList $Collection["GetRequiredSolution"].add("INTL") | out-null } Return $Collection } # Get user configurations Function Get-CAMPCollection { Param ( [String]$LogFile, [System.Collections.ArrayList] $GeoList, [System.Collections.ArrayList] $SolutionList ) $Collection = @{} [CAMPService]$Collection["Services"] = [CAMPService]::DLP try { Write-EXOPAdminAuditLog -Comment "Configuration Analyzer for Microsoft Purview Started at- $(Get-Date)" } catch { $ErrorMessage = $_.ToString() $StackTraceInfo = $_.ScriptStackTrace Write-Log -IsError -ErrorMessage $ErrorMessage -StackTraceInfo $StackTraceInfo -LogFile $LogFile -ErrorAction:SilentlyContinue } if ($SolutionList -icontains "DLP") { $InfoMessage = "Getting DLP Settings" Write-Host "$(Get-Date) $InfoMessage" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue $Collection = Get-DataLossPreventionSettings -Collection $Collection -LogFile $LogFile } if ($SolutionList -icontains "IP") { $InfoMessage = "Getting Information Protection Settings" Write-Host "$(Get-Date) $InfoMessage" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue $Collection = Get-InformationProtectionSettings -Collection $Collection -LogFile $LogFile } if ($SolutionList -icontains "CC") { $InfoMessage = "Getting Communication Compliance Settings" Write-Host "$(Get-Date) $InfoMessage" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue $Collection = Get-CommunicationComplianceSettings -Collection $Collection -LogFile $LogFile } if (($SolutionList -icontains "IG") -or ($SolutionList -icontains "RM")) { $InfoMessage = "Getting Information Governance Settings" Write-Host "$(Get-Date) $InfoMessage" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue $Collection = Get-InformationGovernanceSettings -Collection $Collection -LogFile $LogFile } if ($SolutionList -icontains "Audit" ) { $InfoMessage = "Getting Audit Settings" Write-Host "$(Get-Date) $InfoMessage" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue $Collection = Get-AuditSettings -Collection $Collection -LogFile $LogFile } if ($SolutionList -icontains "eDiscovery") { $InfoMessage = "Getting eDiscovery Settings" Write-Host "$(Get-Date) $InfoMessage" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue $Collection = Get-eDiscoverySettings -Collection $Collection -LogFile $LogFile } if ($SolutionList -icontains "IRM") { $InfoMessage = "Getting Insider Risk Management Settings" Write-Host "$(Get-Date) $InfoMessage" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue $Collection = Get-InsiderRiskManagementSettings -Collection $Collection -LogFile $LogFile } $InfoMessage = "Getting Accepted Domains" Write-Host "$(Get-Date) $InfoMessage" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue $Collection = Get-AcceptedDomains -Collection $Collection -LogFile $LogFile $InfoMessage = "Getting Alert Policies Settings" Write-Host "$(Get-Date) $InfoMessage" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue $Collection = Get-AlertPolicies -Collection $Collection -LogFile $LogFile $InfoMessage = "Getting Organization's region information" Write-Host "$(Get-Date) $InfoMessage" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue $Collection = Get-OrganisationRegion -GeoList $GeoList -Collection $Collection -LogFile $LogFile $InfoMessage = "Getting Organization's solution preference information" Write-Host "$(Get-Date) $InfoMessage" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue $Collection = Get-PersonalizedSolution -SolutionList $SolutionList -Collection $Collection -LogFile $LogFile Return $Collection } Function Get-CAMPReport { <# .SYNOPSIS The Configuration Analyzer for Microsoft Purview (CAMP) .DESCRIPTION Configuration Analyzer for Microsoft Purview (CAMP) The Get-CAMPReport command generates a HTML report highlighting known issues in your compliance configurations in achieving data protection guidelines and recommends best practices to follow. Output report uses open source components for HTML formatting: - Bootstrap - MIT License https://getbootstrap.com/docs/4.0/about/license/ - Fontawesome - CC BY 4.0 License - https://fontawesome.com/license/free .PARAMETER NoVersionCheck Prevents Configuration Analyzer for Microsoft Purview from determining if it's running the latest version. It's always very important to be running the latest version of Configuration Analyzer for Microsoft Purview. We will change guidelines as the product and the recommended practices article changes. Not running the latest version might provide recommendations that are no longer valid. .PARAMETER TurnOffDataCollection Disables data collection. It can be used by users who wish to turn off data collection by Microsoft. Turning it off will delete the UserConsent file present in the output Report folder and ultimately will not consider acceptance in further running instance of the tool. .PARAMETER Geo This will generate a report based on the geolocations entered by you.You need to input appropriate numbers from the following list corresponding to the regions. Input Region 1 Asia-Pacific 2 Australia 3 Canada 4 Europe (excl. France) / Middle East / Africa 5 France 6 India 7 Japan 8 Korea 9 North America (excl. Canada) 10 South America 11 South Africa 12 Switzerland 13 United Arab Emirates 14 United Kingdom .PARAMETER Solution This will generate a report only for the solutions entered by you. You need to input appropriate numbers from the following list corresponding to the solution. Input Solution 1 Data Loss Prevention 2 Information Protection 3 Information Governance 4 Records Management 5 Communication Compliance 6 Insider Risk Management 7 Audit 8 eDiscovery .PARAMETER ExchangeEnvironmentName This will generate CAMP report for Security & Compliance Center PowerShell in a Microsoft 365 DoD organization or Microsoft GCC High organization O365USGovDoD This will generate CAMP report for Security & Compliance Center PowerShell in a Microsoft 365 DoD organization. O365USGovGCCHigh This will generate CAMP report for Security & Compliance Center PowerShell in a Microsoft GCC High organization. .PARAMETER Collection Internal only. .EXAMPLE Get-CAMPReport This will generate a customized report based on the geolocation of your tenant. If an error occurs while fetching your tenant's geolocation, you will get a report covering all supported geolocations. .EXAMPLE Get-CAMPReport -Geo @(1,7) This will generate a customized report based on the geolocations entered by you. .EXAMPLE Get-CAMPReport -Solution @(1,7) This will generate a customized report for the solutions entered by you. .EXAMPLE Get-CAMPReport -Solution @(1,7) -Geo @(9) This will generate a report only on for the solutions entered by you and based on the regions you have selected. #> Param( [CmdletBinding()] [Switch]$NoVersionCheck, [Switch]$TurnOffDataCollection, [System.Collections.ArrayList] $Geo = @(), [System.Collections.ArrayList] $Solution = @(), [string][validateset('O365Default', 'O365USGovDoD', 'O365USGovGCCHigh')] $ExchangeEnvironmentName = 'O365Default', $Collection ) $OutputDirectoryName = Get-CAMPDirectory if(($TurnOffDataCollection -eq $true) -and ($(Test-Path -Path "$OutputDirectoryName\UserConsent.txt" -PathType Leaf) -eq $true)) { Remove-Item "$OutputDirectoryName\UserConsent.txt" } if ((Test-Path -Path "$OutputDirectoryName\UserConsent.txt" -PathType Leaf) -and ($(Get-Content "$OutputDirectoryName\UserConsent.txt") -ieq "Yes")) { $global:TelemetryEnabled = $true } else { $cntOfIterations = 1 Write-Host "Data Collection: The software may collect information about you and your use of the software and send it to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may turn off the telemetry as described in the repository. There are also some features in the software that may enable you and Microsoft to collect data from users of your applications. If you use these features, you must comply with applicable law, including providing appropriate notices to users of your applications together with a copy of Microsoft's privacy statement. Our privacy statement is located at https://go.microsoft.com/fwlink/?LinkID=824704. You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices." -ForegroundColor Yellow while ($cntOfIterations -lt 3) { Write-Host "Do you accept(Y/N):" -NoNewline -ForegroundColor Yellow $telemetryConsent = Read-Host -ErrorAction:SilentlyContinue $telemetryConsent = $telemetryConsent.Trim() if (($telemetryConsent -ieq "y") -or ($telemetryConsent -ieq "yes")) { if (Test-Path -Path "$OutputDirectoryName\UserConsent.txt" -PathType Leaf) { Remove-Item "$OutputDirectoryName\UserConsent.txt" } New-Item "$OutputDirectoryName\UserConsent.txt" | Out-Null Set-Content "$OutputDirectoryName\UserConsent.txt" 'Yes' $global:TelemetryEnabled = $true break } elseif (($telemetryConsent -ieq "n") -or ($telemetryConsent -ieq "no")) { break } Write-Host "Invalid input! Please try again." -ForegroundColor Red $cntOfIterations += 1 } if ($cntOfIterations -eq 3) { return } } $global:EnvironmentName = $ExchangeEnvironmentName $LogDirectory = "$OutputDirectoryName\Logs" $FileName = "CAMP-$(Get-Date -Format 'yyyyMMddHHmmss').log" $LogFile = "$LogDirectory\$FileName" #Creating the logfiles folder if not present if ($(Test-Path -Path $LogDirectory) -eq $false) { New-Item -Path $LogDirectory -ItemType Directory -ErrorAction:SilentlyContinue | Out-Null #Creating the logfile New-Item -Path $LogFile -ItemType File -ErrorAction:SilentlyContinue | Out-Null } else { New-Item -Path $LogFile -ItemType File -ErrorAction:SilentlyContinue | Out-Null } #Check if log file exists if ($(Test-Path -Path $LogFile) -eq $False) { Write-Host "$(Get-Date) Log file cannot be created." -ForegroundColor:Red } Write-Log -MachineInfo -LogFile $LogFile -ErrorAction:SilentlyContinue if (($(Get-GeoAcceptance -Geo $Geo) -eq $false ) -and ($(Get-SolutionAcceptance -Solution $Solution) -eq $false)) { Show-GeoOptions Show-SolutionOptions return } #Get actual region names [System.Collections.ArrayList] $GeoList = @() if (($(Get-GeoAcceptance -Geo $Geo) -eq $false )) { Show-GeoOptions return } else { #Number To Region Mapping $NumberToRegionMapping = Get-NumberRegionMappingHashTable #Mapping numbers to the actual region foreach ($RegionNumber in $Geo) { [string] $RegionName = $NumberToRegionMapping[$RegionNumber].Code $GeoList.add($RegionName) | out-null } } #Get actual region names [System.Collections.ArrayList] $SolutionList = @() if ($(Get-SolutionAcceptance -Solution $Solution) -eq $false) { Show-SolutionOptions return } else { $ShowSolutionList = "" $SolutionTable = Get-SolutionTable if ($Solution.count -gt 0) { foreach ($count in $Solution) { [string] $Name = "$($($SolutionTable[$count]).Code)" #write-host "$Name" $SolutionList.add($Name) | out-null $ShowSolutionList += "$($($SolutionTable[$count]).FullName), " } $ShowSolutionList = $ShowSolutionList.TrimEnd(", ") } else { [int] $count = 1 while ($count -le 8) { $SolutionList.add($($($SolutionTable[$count]).Code)) | out-null $count = $count + 1 } $ShowSolutionList += "All Solutions" } } # Easy to use for quick Configuration Analyzer for Microsoft Purview report to HTML If ($NoVersionCheck) { $PerformVersionCheck = $False } Else { $PerformVersionCheck = $True } try { $Result = Invoke-CAMP -PerformVersionCheck $PerformVersionCheck -Collection $Collection -Output @("HTML") -GeoList $GeoList -SolutionList $SolutionList -LogFile $LogFile -ExchangeEnvironmentName $ExchangeEnvironmentName-ErrorAction:SilentlyContinue $InfoMessage = "Complete! Output is in $($Result.Result)" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue Write-Host "$(Get-Date) $InfoMessage" try { Write-EXOPAdminAuditLog -Comment "Configuration Analyzer for Microsoft Purview Completed at - $(Get-Date)" } catch { $ErrorMessage = $_.ToString() $StackTraceInfo = $_.ScriptStackTrace Write-Log -IsError -ErrorMessage $ErrorMessage -StackTraceInfo $StackTraceInfo -LogFile $LogFile -ErrorAction:SilentlyContinue } } catch { Write-Host "Error:$(Get-Date) There was an issue in running the tool. Please try running the tool again after some time." -ForegroundColor:Red Write-Host "Please refer to the documentation and FAQs available at https://github.com/OfficeDev/CAMP/blob/main/README.md to get guidance for resolving common issues. If the issue persists, please write to us at CAMPhelp@microsoft.com along with log file at $LogFile" -ForegroundColor:Red $ErrorMessage = $_.ToString() $StackTraceInfo = $_.ScriptStackTrace Write-Log -IsError -ErrorMessage $ErrorMessage -StackTraceInfo $StackTraceInfo -LogFile $LogFile -ErrorAction:SilentlyContinue } finally { Write-Log -StopInfo -LogFile $LogFile -ErrorAction:SilentlyContinue $InfoMessage = "Get the log at $LogFile" Write-Host "$(Get-Date) $InfoMessage" try { if($($global:ConnectionEstablished) -eq $true) { Disconnect-ExchangeOnline -Confirm:$false -ErrorAction:SilentlyContinue } } catch { } } } Function Invoke-CAMP { Param( [CmdletBinding()] [Boolean]$PerformVersionCheck = $True, $Output, $OutputOptions, $Collection, [System.Collections.ArrayList] $GeoList = @(), [System.Collections.ArrayList] $SolutionList = @(), [String]$LogFile ) $InfoMessage = "Configuration Analyzer for Microsoft Purview Started" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue # Version check If ($PerformVersionCheck) { $InfoMessage = "Version Check Started" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue $VersionCheck = Invoke-CAMPVersionCheck $InfoMessage = "Version Check Completed" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue } $InfoMessage = "Establishing Connections" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue Invoke-CAMPConnections -LogFile $LogFile -ExchangeEnvironmentName $ExchangeEnvironmentName $InfoMessage = "Connections Established" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue # Get the collection in to memory. For testing purposes, we support passing the collection as an object If ($Null -eq $Collection) { $InfoMessage = "Fetching User Configurations" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue $Collection = Get-CAMPCollection -GeoList $GeoList -SolutionList $SolutionList -LogFile $LogFile $InfoMessage = "User Configurations Fetched" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue } # Get the output modules $InfoMessage = "Creating Output Objects" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue $OutputModules = Get-CAMPOutputs -VersionCheck $VersionCheck -Modules $Output -Options $OutputOptions $InfoMessage = "Output Objects Created" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue # Get the object of Configuration Analyzer for Microsoft Purview checks $InfoMessage = "Creating Check Objects" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue $Checks = Get-CAMPCheckDefs -CAMPParams $CAMPParams -Collection $Collection -LogFile $LogFile $InfoMessage = "Check Objects Created" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue # Perform checks inside classes/modules ForEach ($Check in ($Checks | Sort-Object Area)) { # Run DLP checks by default if ($check.Services -band [CAMPService]::DLP) { $Check.Run($Collection) } } # Get the Remedition Steps $InfoMessage = "Creating Remediation Objects" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue $RemediationActionModules = Get-CAMPRemediationAction -VersionCheck $VersionCheck $InfoMessage = "Remediation Objects Created" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue ForEach ($a in $RemediationActionModules) { $a.Run($Checks, $Collection) } $TenantGeoLocations = $Collection["GetOrganisationRegion"] | Where-Object { $_ -ne "INTL" } if ($TenantGeoLocations -ne "Error") { $RegionString = "" $NumberToRegionMapping = Get-NumberRegionMappingHashTable foreach ($Region in $TenantGeoLocations) { foreach ($Numbers in $($NumberToRegionMapping.Keys)) { if ($($NumberToRegionMapping[$Numbers].Code) -eq $Region) { if ($RegionString -eq "") { $RegionString += "$($NumberToRegionMapping[$Numbers].Description)" } else { $RegionString += ", $($NumberToRegionMapping[$Numbers].Description)" } } } } } else { $RegionString = "" $RegionString += "All Geolocations" } $InfoMessage = "The following report is generated for following solutions:$ShowSolutionList" Write-Host "$(Get-Date) $InfoMessage" -ForegroundColor Yellow $InfoMessage = "The following report is for following geolocations:$RegionString" Write-Host "$(Get-Date) $InfoMessage" -ForegroundColor Yellow $OutputResults = @() $InfoMessage = "Generating Output" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue Write-Host "$(Get-Date) $InfoMessage" -ForegroundColor Green # Perform required outputs ForEach ($o in $OutputModules) { $o.Run($Checks, $Collection) $OutputResults += New-Object -TypeName PSObject -Property @{ Name = $o.name Completed = $o.completed Result = $o.Result } } # If Telemetry is enabled (For Customers), then collect telemetry if ($($global:TelemetryEnabled) -eq $true) { $InfoMessage = "Collecting Telemetry" Write-Log -IsInfo -InfoMessage $InfoMessage -LogFile $LogFile -ErrorAction:SilentlyContinue $CAMPVersion = $VersionCheck.Version.ToString() # Setting tenant name if (($Collection["AcceptedDomains"] -eq "Error") -or ($Collection["AcceptedDomains"] -eq "") -or ($null -eq $Collection["AcceptedDomains"]) ) { if($null -ne $global:UserName) { if($global:UserName.Contains("@")) { $DomainName = $global:UserName.Split("@")[1]; } else { $DomainName = "Error" } } else { $DomainName = "Error" } } else { $DomainName = ($Collection["AcceptedDomains"] | Where-Object { $_.InitialDomain -eq $True }).DomainName } # Setting organization name if ($Collection["GetOrganisationConfig"] -eq "Error") { $OrganizationName = "Error" } else { $OrganizationName = $Collection["GetOrganisationConfig"].DisplayName } $SolutionSummaryResult = @{} ForEach ($Area in ($Checks | Where-Object { $_.Completed -eq $true } | Group-Object Area)) { if($($Area.Name) -eq "Compliance Manager") { continue } $Pass = @($Area.Group | Where-Object { $_.Result -eq "Pass" }).Count $Fail = @($Area.Group | Where-Object { $_.Result -eq "Fail" }).Count $Info = @($Area.Group | Where-Object { $_.Result -eq "Recommendation" }).Count $SolutionSummaryResult[$($Area.Name)] = New-Object -TypeName PSObject -Property @{ Pass = $Pass Info = $Info Fail = $Fail } } # Set the parameter for the URI $Parameters = @{ CAMPVersion = $CAMPVersion Domain = $DomainName Organization = $OrganizationName } $AllSolutions = Get-SolutionTable foreach ($solution in $($AllSolutions.Values.FullName)) { $solutionName = $solution -replace '\s', '' if ($SolutionSummaryResult.ContainsKey($solution)) { $Parameters.Add($($solutionName + "_Pass"), $SolutionSummaryResult[$solution].Pass) $Parameters.Add($($solutionName + "_Info"), $SolutionSummaryResult[$solution].Info) $Parameters.Add($($solutionName + "_Fail"), $SolutionSummaryResult[$solution].Fail) } else { $Parameters.Add($($solutionName + "_Pass"), 0) $Parameters.Add($($solutionName + "_Info"), 0) $Parameters.Add($($solutionName + "_Fail"), 0) } } $Parameters = $Parameters | ConvertTo-Json try { # URI to trigger the Telemetry Function $URI = "https://mccatelemetryapi.azure-api.net/MCCATelemetryServiceApp/MCCAFunction" # Call the URI $ResponseMessage = Invoke-WebRequest -Uri $URI -ContentType "application/json" -Method POST -Body $Parameters -ErrorAction:SilentlyContinue Write-Log -IsInfo -InfoMessage $ResponseMessage -LogFile $LogFile -ErrorAction:SilentlyContinue } catch { $ResponseMessage = "Telemetry execution failed!" Write-Log -IsInfo -InfoMessage $ResponseMessage -LogFile $LogFile -ErrorAction:SilentlyContinue } } Return $OutputResults } function Invoke-CAMPVersionCheck { Param ( $Terminate ) Write-Host "$(Get-Date) Performing Configuration Analyzer for Microsoft Purview Version check... " # When detected we are running the preview release $CAMP = "" $CAMPVersion = "" $PSGalleryVersionNotFound = $False $CAMPVersionNotFound =$False try { $CAMPVersion = (Get-InstalledModule CAMP -ErrorAction:SilentlyContinue -WarningAction:SilentlyContinue | Sort-Object Version -Desc)[0].Version } catch { $CAMPVersionNotFound = $True } try { $PSGalleryVersion = (Find-Module CAMP -Repository PSGallery -ErrorAction:SilentlyContinue -WarningAction:SilentlyContinue).Version } catch { $PSGalleryVersionNotFound = $True } If ($CAMPVersionNotFound) { $Updated = $False Throw "CAMP is not installed. Run Install-Module CAMP." } elseif ($PSGalleryVersionNotFound) { Throw "There was some issue in running the tool. Please try after some time." } elseif ($PSGalleryVersion -gt $CAMPVersion) { $Updated = $False If ($Terminate) { Throw "CAMP is out of date. Your version is $CAMPVersion and the published version is $PSGalleryVersion. Run Update-Module CAMP." } else { Write-Host "$(Get-Date) CAMP is out of date. Your version: $($CAMPVersion) published version is $($PSGalleryVersion)" } } else { $Updated = $True } Return New-Object -TypeName PSObject -Property @{ Updated = $Updated Version = $CAMPVersion GalleryVersion = $PSGalleryVersion Preview = $Preview } } #Creating log file and directory #Writing in log file function Write-Log { [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [Switch]$IsError = $false, [Parameter(Mandatory = $false)] [Switch]$IsWarn = $false, [Parameter(Mandatory = $false)] [Switch]$IsInfo = $false, [Parameter(Mandatory = $false)] [Switch]$MachineInfo = $false, [Parameter(Mandatory = $false)] [Switch]$StopInfo = $false, [Parameter(Mandatory = $false)] [string]$ErrorMessage, [Parameter(Mandatory = $false)] [System.Collections.ArrayList]$WarnMessage, [Parameter(Mandatory = $false)] [string]$InfoMessage, [Parameter(Mandatory = $false)] [string]$StackTraceInfo, [String]$LogFile ) if ($MachineInfo) { $ComputerInfoObj = Get-ComputerInfo $CompName = $ComputerInfoObj.CsName $OSName = $ComputerInfoObj.OsName $OSVersion = $ComputerInfoObj.OsVersion $PowerShellVersion = $PSVersionTable.PSVersion try { "********************************************************************************************" | Out-File $LogFile -Append -ErrorAction:SilentlyContinue "Logging Started" | Out-File $LogFile -Append -ErrorAction:SilentlyContinue "Start time: $(Get-Date)" | Out-File $LogFile -Append -ErrorAction:SilentlyContinue "Computer Name: $CompName" | Out-File $LogFile -Append -ErrorAction:SilentlyContinue "Operating System Name: $OSName" | Out-File $LogFile -Append -ErrorAction:SilentlyContinue "Operating System Version: $OSVersion" | Out-File $LogFile -Append -ErrorAction:SilentlyContinue "PowerShell Version: $PowerShellVersion" | Out-File $LogFile -Append -ErrorAction:SilentlyContinue "********************************************************************************************" | Out-File $LogFile -Append -ErrorAction:SilentlyContinue } catch { Write-Host "$(Get-Date) The local machine information cannot be logged." -ForegroundColor:Yellow } } if ($StopInfo) { try { "********************************************************************************************" | Out-File $LogFile -Append -ErrorAction:SilentlyContinue "Logging Ended" | Out-File $LogFile -Append -ErrorAction:SilentlyContinue "End time: $(Get-Date)" | Out-File $LogFile -Append -ErrorAction:SilentlyContinue "********************************************************************************************" | Out-File $LogFile -Append -ErrorAction:SilentlyContinue if ($($global:ErrorOccurred) -eq $true) { Write-Host "Warning:$(Get-Date) The report generated may have reduced information due to errors in running the tool. These errors may occur due to multiple reasons. Please refer documentation for more details." -ForegroundColor:Yellow } } catch { Write-Host "$(Get-Date) The finishing time information cannot be logged." -ForegroundColor:Yellow } } #Error if ($IsError) { if ($($global:ErrorOccurred) -eq $false) { $global:ErrorOccurred = $true } $Log_content = "$(Get-Date) ERROR: $ErrorMessage" try { $Log_content | Out-File $LogFile -Append -ErrorAction:SilentlyContinue "TRACE: $StackTraceInfo" | Out-File $LogFile -Append -ErrorAction:SilentlyContinue } catch { Write-Host "$(Get-Date) An error event cannot be logged." -ForegroundColor:Yellow } } #Warning if ($IsWarn) { foreach ($Warnmsg in $WarnMessage) { $Log_content = "$(Get-Date) WARN: $Warnmsg" try { $Log_content | Out-File $LogFile -Append -ErrorAction:SilentlyContinue } catch { Write-Host "$(Get-Date) A warning event cannot be logged." -ForegroundColor:Yellow } } } #General if ($IsInfo) { $Log_content = "$(Get-Date) INFO: $InfoMessage" try { $Log_content | Out-File $LogFile -Append -ErrorAction:SilentlyContinue } catch { Write-Host "$(Get-Date) A general event cannot be logged." -ForegroundColor:Yellow } } } # Get the Number Region Mapping HashTable function Get-NumberRegionMappingHashTable { #Number To Region Mapping $NumberToRegionMapping = @{} $NumberToRegionMapping[1] = New-Object -TypeName PSObject -Property @{ Code = "APC" Description = "Asia-Pacific" } $NumberToRegionMapping[2] = New-Object -TypeName PSObject -Property @{ Code = "AUS" Description = "Australia" } $NumberToRegionMapping[3] = New-Object -TypeName PSObject -Property @{ Code = "CAN" Description = "Canada" } $NumberToRegionMapping[4] = New-Object -TypeName PSObject -Property @{ Code = "EUR" Description = "Europe (excl. France) / Middle East / Africa" } $NumberToRegionMapping[5] = New-Object -TypeName PSObject -Property @{ Code = "FRA" Description = "France" } $NumberToRegionMapping[6] = New-Object -TypeName PSObject -Property @{ Code = "IND" Description = "India" } $NumberToRegionMapping[7] = New-Object -TypeName PSObject -Property @{ Code = "JPN" Description = "Japan" } $NumberToRegionMapping[8] = New-Object -TypeName PSObject -Property @{ Code = "KOR" Description = "Korea" } $NumberToRegionMapping[9] = New-Object -TypeName PSObject -Property @{ Code = "NAM" Description = "North America (excl. Canada)" } $NumberToRegionMapping[10] = New-Object -TypeName PSObject -Property @{ Code = "LAM" Description = "South America" } $NumberToRegionMapping[11] = New-Object -TypeName PSObject -Property @{ Code = "ZAF" Description = "South Africa" } $NumberToRegionMapping[12] = New-Object -TypeName PSObject -Property @{ Code = "CHE" Description = "Switzerland" } $NumberToRegionMapping[13] = New-Object -TypeName PSObject -Property @{ Code = "ARE" Description = "United Arab Emirates" } $NumberToRegionMapping[14] = New-Object -TypeName PSObject -Property @{ Code = "GBR" Description = "United Kingdom" } return $NumberToRegionMapping } #Check if the geo param is in right format function Get-GeoAcceptance { param ( $Geo ) $LegitimateGeo = $Geo | Where-Object { ($_ -ge 1) -and ($_ -le 14) } return ($($LegitimateGeo.Count) -eq $($Geo.Count)) } # Display options for the user to choose function Show-GeoOptions { Write-Host "Error:$(Get-Date) Please input appropriate numbers from the following list corresponding to the regions for which you wish to customize the report & run the tool again." -ForegroundColor:Red #Number To Region Mapping $NumberToRegionMapping = Get-NumberRegionMappingHashTable Write-Host "*******************************************************************************" write-host "For Geo Location" Write-Host "*******************************************************************************" [int] $count = 1 while ($count -le 14) { Write-Host "$count--->$($($NumberToRegionMapping[$count]).Description)" $count = $count + 1 } Write-Host "*******************************************************************************" Write-Host "Example: Get-CAMPReport -Geo @(1,7) -Solution @(1,7)" Write-Host "or" Write-Host "Get-CAMPReport -Geo @(1,7)" Write-Host "" Write-Host "" } function Get-SolutionTable { #Number To Region Mapping $SolutionTable = @{} $SolutionTable[1] = New-Object -TypeName PSObject -Property @{ Code = "DLP" FullName = "Data Loss Prevention" } $SolutionTable[2] = New-Object -TypeName PSObject -Property @{ Code = "IP" FullName = "Information Protection" } $SolutionTable[3] = New-Object -TypeName PSObject -Property @{ Code = "IG" FullName = "Information Governance" } $SolutionTable[4] = New-Object -TypeName PSObject -Property @{ Code = "RM" FullName = "Records Management" } $SolutionTable[5] = New-Object -TypeName PSObject -Property @{ Code = "CC" FullName = "Communication Compliance" } $SolutionTable[6] = New-Object -TypeName PSObject -Property @{ Code = "IRM" FullName = "Insider Risk Management" } $SolutionTable[7] = New-Object -TypeName PSObject -Property @{ Code = "Audit" FullName = "Audit" } $SolutionTable[8] = New-Object -TypeName PSObject -Property @{ Code = "eDiscovery" FullName = "eDiscovery" } return $SolutionTable } #Check if the geo param is in right format function Get-SolutionAcceptance { Param ( $Solution ) $ValidSolution = $Solution | Where-Object { ($_ -ge 1) -and ($_ -le 8) } return ($($ValidSolution.Count) -eq $($Solution.Count)) } function Show-SolutionOptions { Write-Host "Error:$(Get-Date) Please input appropriate numbers from the following list corresponding to solution for which you wish to customize the report & run the tool again." -ForegroundColor:Red $SolutionTable = Get-SolutionTable Write-Host "*******************************************************************************" write-host "Solution" Write-Host "*******************************************************************************" [int] $count = 1 while ($count -le 8) { Write-Host "$count--->$($($SolutionTable[$count]).FullName)" $count = $count + 1 } Write-Host "*******************************************************************************" Write-Host "Example: Get-CAMPReport -Geo @(1,7) -Solution @(1,7)" Write-Host "or" Write-Host "Get-CAMPReport -Solution @(1,7)" Write-Host "" Write-Host "" } # SIG # Begin signature block # MIIlvQYJKoZIhvcNAQcCoIIlrjCCJaoCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCBqdnUqREqBINC # D9TLmDCHP2XpDCnblGs0FRJaT8Xcg6CCC5YwggT7MIID46ADAgECAhMzAAAE4qOH # mdFtQCqWAAEAAATiMA0GCSqGSIb3DQEBCwUAMHkxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xIzAhBgNVBAMTGk1pY3Jvc29mdCBUZXN0aW5nIFBD # QSAyMDEwMB4XDTIxMDkwMjE5MjYyN1oXDTIyMDkwMTE5MjYyN1owfDELMAkGA1UE # BhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAc # BgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdQ29kZSBTaWdu # IFRlc3QgKERPIE5PVCBUUlVTVCkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK # AoIBAQC08O5FqM6i91zE1YIK4epCwArU+0xTEScHJ0Vfy32sK+Xf3dM/vHm1iLfc # 7hhDNwVxfHvaHgjNYC2Qr2cff4F40aShGrc6xyIdmcSiygICYGQrF7FWKQ4thEdX # HuAMiuVYZQ9Jo+vxOXRJTSa3zsFFrbkCOgun7Vu9UeFxOQT1ByNomsxky0gCRu+P # YdaqnbOyxNsnCDaGUUHp+D1EpBv4Cdrgy9o359Z4nFUyUm4MVMjpG6jC2n8dPonw # hAcbjIvyJzA1v88jVCMfDrHt0AkIHN69J4wahtOrJeERgXjklilV0hMRwkVkfpnJ # 18aw7NqWsDi4ekmBd2EL6d+HljzNAgMBAAGjggF3MIIBczATBgNVHSUEDDAKBggr # BgEFBQcDAzAdBgNVHQ4EFgQUCekP9tRzR3NLSbCNG+oPPWd/nuYwRQYDVR0RBD4w # PKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEWMBQGA1UEBRMN # MjMwMDcyKzQ2NzYwNTAfBgNVHSMEGDAWgBS/ZaKrb3WjTkWWVwXPOYf0wBUcHDBc # BgNVHR8EVTBTMFGgT6BNhktodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3Bz # L2NybC9NaWNyb3NvZnQlMjBUZXN0aW5nJTIwUENBJTIwMjAxMCgxKS5jcmwwaQYI # KwYBBQUHAQEEXTBbMFkGCCsGAQUFBzAChk1odHRwOi8vd3d3Lm1pY3Jvc29mdC5j # b20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMFRlc3RpbmclMjBQQ0ElMjAyMDEw # KDEpLmNydDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCTxe9mJYSi # GUIIw4imC+gePdQLAeeMTTa8Uwa406/66Su7fVZTGNFMpLwofA1WfxvAX4+5+U+J # aFJod4KyAdUngWtPlyPmkR0kXQ6mTxvqMFEih/s0mRyW6dHMHrCezmY//89C5zau # E3LBcI4gqAQ2ZabPxj2+j6+Er8StKv07PigCuM4M16DIQUAY6kNgnate4WoiiQSu # mXhMiwtUh6jgSnTpOqpNfXk+YJxNoqHH8qqyW7FcQ88jyoxM4LYdAQVN3eYD7eJn # xfW2nWIbDRaZpBkv9N1t8yObWcyC/YrRLuq3QLqpll20Y1eFsm+gypEoiERqpPUT # 3p7K5897ABKoMIIGkzCCBHugAwIBAgITMwAAAC01ekaIyQdx2AAAAAAALTANBgkq # hkiG9w0BAQsFADCBkDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x # EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv # bjE6MDgGA1UEAxMxTWljcm9zb2Z0IFRlc3RpbmcgUm9vdCBDZXJ0aWZpY2F0ZSBB # dXRob3JpdHkgMjAxMDAeFw0yMDEyMTAyMDQzMjBaFw0zNTA2MTcyMTA0MTFaMHkx # CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt # b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xIzAhBgNVBAMTGk1p # Y3Jvc29mdCBUZXN0aW5nIFBDQSAyMDEwMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A # MIIBCgKCAQEAvzxggau+7P/XF2PypkLRE2KcsBfOukYaeyIuVXOaVLnG1NHKmP53 # Rw2OnfBezPhU7/LPKtRi8ak0CgTXxQWG8hD1TdOWCGaF2wJ9GNzieiOnmildrnkY # zwxj8Br/gampQz+pC7lR8bNIOvxELl8RxVY6/8oOzYgIwf3H1fU+7+pOG3KLI71F # N54fcMGnybggc+3zbD2LIQXPdxL+odwH6Q1beAlsMlUQR9A3yMf3+nP+RjTkVhao # N2RT1jX7w4C2jraGkaEQ1sFK9uN61BEKst4unhCX4IGuEl2IAV3MpMQoUpxg8Arm # iK9L6VeK7KMPNx4p9l0h09faXQ7JTtuNbQIDAQABo4IB+jCCAfYwDgYDVR0PAQH/ # BAQDAgGGMBIGCSsGAQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUCBBYEFOqfXzO2 # 0F+erestpsECu0A4y+e1MB0GA1UdDgQWBBS/ZaKrb3WjTkWWVwXPOYf0wBUcHDBU # BgNVHSAETTBLMEkGBFUdIAAwQTA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNy # b3NvZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnkuaHRtMBkGCSsGAQQBgjcU # AgQMHgoAUwB1AGIAQwBBMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUowEE # fjCIM+u5MZzK64V2Z/xltNEwWQYDVR0fBFIwUDBOoEygSoZIaHR0cDovL2NybC5t # aWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljVGVzUm9vQ2VyQXV0XzIw # MTAtMDYtMTcuY3JsMIGNBggrBgEFBQcBAQSBgDB+ME0GCCsGAQUFBzAChkFodHRw # Oi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Rlc1Jvb0NlckF1dF8y # MDEwLTA2LTE3LmNydDAtBggrBgEFBQcwAYYhaHR0cDovL29uZW9jc3AubWljcm9z # b2Z0LmNvbS9vY3NwMA0GCSqGSIb3DQEBCwUAA4ICAQAntNCFsp7MD6QqU3PVbdrX # MQDI9v9jyPYBEbUYktrctPmvJuj8Snm9wWewiAN5Zc81NQVYjuKDBpb1un4SWVCb # 4PDVPZ0J87tGzYe9dOJ30EYGeiIaaStkLLmLOYAM6oInIqIwVyIk2SE/q2lGt8Ov # wcZevNmPkVYjk6nyJi5EdvS6ciPRmW9bRWRT4pWU8bZIQL938LE4lHOQAixrAQiW # es5Szp2U85E0nLdaDr5w/I28J/Z1+4zW1Nao1prVCOqrosnoNUfVf1kvswfW3FY2 # l1PiAYp8sGyO57GaztXdBoEOBcDLedfcPra9+NLdEF36NkE0g+9dbokFY7KxhUJ8 # WpMiCmN4yj9LKFLvQbctGMJJY9EwHFifm2pgaiaafKF1Gyz+NruJzEEgpysMo/f9 # AVBQ/qCdPQQGEWp3QDIaef4ts9QTx+RmDKCBDMTFLgFmmhbtUY0JWjLkKn7soz/L # IcDUle/p5TiFD4VhfZnAcvYQHXfuslnyp+yuhWzASnAQNnOIO6fc1JFIwkDkcM+k # /TspfAajzHooSAwXkrOWrjRDV6wI0YzMVHrEyQ0hZ5NnIXbL3lrTkOPjf3NBu1na # SNEaySduStDbFVjV3TXoENEnZiugJKYSwmhzoYHM1ngipN5rNdqJiK5ukp6E8LDz # i3l5/7XctJQY3+ZgHDJosjGCGX0wghl5AgEBMIGQMHkxCzAJBgNVBAYTAlVTMRMw # EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN # aWNyb3NvZnQgQ29ycG9yYXRpb24xIzAhBgNVBAMTGk1pY3Jvc29mdCBUZXN0aW5n # IFBDQSAyMDEwAhMzAAAE4qOHmdFtQCqWAAEAAATiMA0GCWCGSAFlAwQCAQUAoIGu # MBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgor # BgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCDFX9+ZTAvLeGCQshxXg7tyv0kmg1rz # XH5UjuXR7YUTqTBCBgorBgEEAYI3AgEMMTQwMqAUgBIATQBpAGMAcgBvAHMAbwBm # AHShGoAYaHR0cDovL3d3dy5taWNyb3NvZnQuY29tMA0GCSqGSIb3DQEBAQUABIIB # AGlchc5+6VcE2utuAab0t9ucckeyVHGYl6mD+ZbrXxqowWey/U2NX0PAJiarKgT9 # Mdk78fqz/6WWDY2oj7eslx+HjoE9w1AhCvgm8KcKw+bx3cfroPZXeYdYI3XiK0yP # iekXX0MWG6m3z9TLfoy9ylXMObRgs0VWIjzHL8E96tpEFc5LnA3ajIZ7B79GKvzG # eVHbr4ohzjC2TRyv6nFX5WaZlFg2kj89IdzVMWY+FxzAza9q6aOPLUg1jf53J6Xt # /ivu8J/7Uwx4BEfDluVc73cyAyOapl5VHJ/DWnSMA94/DSiAgCMgydj7ka+ugTwT # fmNtd3WHDjDSMJzeL+6bNG2hghcMMIIXCAYKKwYBBAGCNwMDATGCFvgwghb0Bgkq # hkiG9w0BBwKgghblMIIW4QIBAzEPMA0GCWCGSAFlAwQCAQUAMIIBVQYLKoZIhvcN # AQkQAQSgggFEBIIBQDCCATwCAQEGCisGAQQBhFkKAwEwMTANBglghkgBZQMEAgEF # AAQgVszEDXl4SJIY2o55VsA1nGSZrHLbDqLvv3vXWwmFCg4CBmKEtsGFaRgTMjAy # MjA2MTAxMDA1MDcuNzQyWjAEgAIB9KCB1KSB0TCBzjELMAkGA1UEBhMCVVMxEzAR # BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p # Y3Jvc29mdCBDb3Jwb3JhdGlvbjEpMCcGA1UECxMgTWljcm9zb2Z0IE9wZXJhdGlv # bnMgUHVlcnRvIFJpY28xJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNOOkM0QkQtRTM3 # Ri01RkZDMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloIIR # XzCCBxAwggT4oAMCAQICEzMAAAGj+5qzjnuGQ08AAQAAAaMwDQYJKoZIhvcNAQEL # BQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT # B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UE # AxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwHhcNMjIwMzAyMTg1MTE2 # WhcNMjMwNTExMTg1MTE2WjCBzjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp # bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw # b3JhdGlvbjEpMCcGA1UECxMgTWljcm9zb2Z0IE9wZXJhdGlvbnMgUHVlcnRvIFJp # Y28xJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNOOkM0QkQtRTM3Ri01RkZDMSUwIwYD # VQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIICIjANBgkqhkiG9w0B # AQEFAAOCAg8AMIICCgKCAgEA771Nyst7KgpwKots23Ps23Yls/VMTPMyGLziBLpg # sjsIXp1VIDkjVsUIdzDpkXZ4XxCJp/SjZdJd/+OnmyCZRi0xsaLid4C1ZI0rnKG3 # iLJCEHGfRMhCoX42KyIHFb56ExhYRsUj7txJ1f91bD79jz8y726rdcIrk/Yb5mJt # XA5Uf3n4iJMaeahzFaB2zSqUGlrWtiDduXxdV7kvapNSYkG+dCnHZtSu7vSW3b8e # hTlsFssoCEI+nHLieayt05Hca/hRt8Ca2lCe0/vnw1E+GDVsAIfToWW9sjI/z+5G # zfHbfbd1poaklBhChmkAGDrasUNMnj57Tq237Ft++nwz2WjxrVqB/FlDWkhPVWcl # 1o73yBYyIxbrl14VSJRH5aeBBV+/aAuy/qjv45ynPLpEdkibpYQZn0sG3nvU18Kz # HnPQiW+vpLM3RBtpYlMshZtfBtRUph5utcRUUzKG5UZAd6xkH5XBXfzqFiiczGzS # O8zwak5zHTEvLKbjZcD31VKmy6K9MmDijxrUAIltMFUWgQDdWsVJjM51Dq/NfGvH # DqL9PXfyb5cX7Iq0ASeGn5R4AyGXDuM/30QCWAZSXQqRwGNNhPP6MTI+App2tTWh # /mgWL+r1gOWtW/0fgmxV7wYcw6Q9M2gHjTbyPzw4R7jboGx9xcuSLSmE+nuKtbQB # tF0CAwEAAaOCATYwggEyMB0GA1UdDgQWBBRQUfuzPCYIsc9NL0GjaKsucmOLqjAf # BgNVHSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBfBgNVHR8EWDBWMFSgUqBQ # hk5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQl # MjBUaW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmwwbAYIKwYBBQUHAQEEYDBe # MFwGCCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2Nl # cnRzL01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNydDAM # BgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMA0GCSqGSIb3DQEBCwUA # A4ICAQBa8t6naZfDqK4HEa7Q+yy2ZcjmAuaA+RMpCeOBmWyh6Kmy1r2iS7QxNXGU # dV1x0FsVxUcwtFGRQUiR8qdpyXKXl7KPTfB4Ppv+lR8XINkHwBmkZReFNgs1Hw96 # kzrIPqD7QTWcfQyE4agTpcW5+Rufp4h01Ma5bAF4SvYM2IaEMaXBpQfkQvPeG27I # zJYoCBgXbwLiLLKFh2+Ub1U3omcLiZz8Qi3nQEIenxlACTscLdE6W2DWC7k2MZpJ # V2KxqLk4lZ/p7mxhB0ME1gpcl2Id6LU3sr4vmzW9X4Lp3dOwX34A2mKgEMA4acVJ # i3g/661bXWLfuIsstv3bqkIvgvL74ZTTCXNh+fufbZHJCa8PXWjKJkeGZGywMGqw # D6e8JW1+cfXzcN/mgEFWyTNlwlNSMLFpqAMrsCoHpADcfuX/ZIV56p9f8O12V7gH # 689XiWrUIKzQDUsH2WbNLS/GhEO6xjzQNCLXQrdJ9krWHLJqP1ryltkbQGwGnY3B # zjG04MR4FNagDMX2SjhXp3T24UWkmsuhX57xwlT+FPf5KVbt2evl21i/OIdOlm65 # G/gpToXc5DM2kd/twEulYpFdGMZh1WcAZvw3NbrBZONmMwI10IUbumQosw4Z2o8M # V1+T4RgTlFIb3LFLvpw99epEpW0llJwgXrh6OddsUizluHwrATCCB3EwggVZoAMC # AQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNV # BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4w # HAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29m # dCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDEwMB4XDTIxMDkzMDE4MjIy # NVoXDTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp # bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw # b3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAw # ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDk4aZM57RyIQt5osvXJHm9 # DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg4r25PhdgM/9cT8dm95VTcVrifkpa/rg2 # Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPFdvWGUNzBRMhxXFExN6AKOG6N # 7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6GnszrYBbfowQHJ1S/rboYiXc # ag/PXfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBpDco2LXCOMcg1KL3jtIckw+DJ # j361VI/c+gVVmG1oO5pGve2krnopN6zL64NF50ZuyjLVwIYwXE8s4mKyzbnijYjk # lqwBSru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3EXzTdEonW/aUgfX782Z5F37Zy # L9t9X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0lBw0gg/wEPK3Rxjtp+iZfD9M # 269ewvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1qGFphAXPKZ6Je1yh2AuIzGHLX # pyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ+QuJYfM2BjUYhEfb3BvR/bLU # HMVr9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PAPBXbGjfHCBUYP3irRbb1Hode # 2o+eFnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkwEgYJKwYBBAGCNxUBBAUCAwEA # ATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxGNSnPEP8vBO4wHQYDVR0OBBYE # FJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARVMFMwUQYMKwYBBAGCN0yDfQEB # MEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMv # RG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAKBggrBgEFBQcDCDAZBgkrBgEE # AYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB # /zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvXzpoYxDBWBgNVHR8ETzBNMEug # SaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9N # aWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsG # AQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jv # b0NlckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAnVV9/Cqt # 4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0xM7U518JxNj/aZGx80HU5bbsP # MeTCj/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmCVgADsAW+iehp4LoJ7nvfam++ # Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449xvNo32X2pFaq95W2KFUn0CS9 # QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wMnosZiefwC2qBwoEZQhlSdYo2 # wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDSPeZKPmY7T7uG+jIa2Zb0j/aR # AfbOxnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2dY3RILLFORy3BFARxv2T5JL5z # bcqOCb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxnGSgkujhLmm77IVRrakURR6nx # t67I6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+CrvsQWY9af3LwUFJfn6Tvsv4O+S3 # Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokLjzbaukz5m/8K6TT4JDVnK+AN # uOaMmdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL6Xu/OHBE0ZDxyKs6ijoIYn/Z # cGNTTY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggLSMIICOwIBATCB/KGB1KSB0TCB # zjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl # ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEpMCcGA1UECxMg # TWljcm9zb2Z0IE9wZXJhdGlvbnMgUHVlcnRvIFJpY28xJjAkBgNVBAsTHVRoYWxl # cyBUU1MgRVNOOkM0QkQtRTM3Ri01RkZDMSUwIwYDVQQDExxNaWNyb3NvZnQgVGlt # ZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQAeX+leQswBs9qkLBr4Zdzd # KUMNE6CBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9u # MRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp # b24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMA0GCSqG # SIb3DQEBBQUAAgUA5k2GyDAiGA8yMDIyMDYxMDEzMDA1NloYDzIwMjIwNjExMTMw # MDU2WjB3MD0GCisGAQQBhFkKBAExLzAtMAoCBQDmTYbIAgEAMAoCAQACAhK8AgH/ # MAcCAQACAhHKMAoCBQDmTthIAgEAMDYGCisGAQQBhFkKBAIxKDAmMAwGCisGAQQB # hFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJKoZIhvcNAQEFBQADgYEA # mZEyUi4NpGaUZ4WAXcSdnUD+/HolU9BPyUtxTa/VlVaPGqv3yc/G6FzP9710Jn2b # iCdpyokRO3cSmHcnjWr6ZEpD1ZuhtXRs/tbkurisRYWfBo+PL3LHZ8ACD578JtwN # HVP7wgYtnh77ciEqVPceXyN2bao6Y9DxwV82sNhIhmYxggQNMIIECQIBATCBkzB8 # MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVk # bW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1N # aWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAaP7mrOOe4ZDTwABAAAB # ozANBglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEE # MC8GCSqGSIb3DQEJBDEiBCDll3qAnl42cje3tfq7Ug/7WhK/By22CncujDPahv09 # +jCB+gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EIIz4uLAGccwyg53+yBtpjGnC # 8QmVERmX+lM+SXPp643+MIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgT # Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m # dCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENB # IDIwMTACEzMAAAGj+5qzjnuGQ08AAQAAAaMwIgQgOAjMPNRxkzZHFsXA8GTnfDz0 # 8nJZs3lm/PpLZP1u7CgwDQYJKoZIhvcNAQELBQAEggIAoyIpQQXHLaFaUc3cKokA # sN5Uajd7ONCOJdO3YSLdQuxECooreUKlYD/Qp2iX8e+ZEra905NzcFhL5G20zWhc # 6m7lEkq8FB+FqxRalTZG3dD2UR875UyqEPbrs5CoRF5pFz4ONMXA/slMynIWjGFF # Zk0HEUgYHD4gj3qsVAHjkQ9KFmy+lvWIkD90Bw3scjWiW6EUvAfnPMTalTxuJlcX # s+F5NdYH2vmoD/R25AupqHb+N2FBFCw8Nh6YfJJNqXB+wkK2Crn8Isg8CZTo1Rs5 # QP/vJbYPKQZmsnKN2PMFiW8ohz5I5xGONFQJBIy3prOkNPN3uJ1fL5T12Kjk2a/h # Shj80vrxFSFGPMDopg3fFWYLd3OVKH/XLFYjJxiALtpdyCxxpgHaYSNuigS0/7y8 # I6RFovSDP6KZUKs0jqHHjr9DkXLBUEyOFaaEDiB/TNHEm2jHCGlzf3g7zqwdKIWn # xmVbXlwekSMqaliH0S53fwBa1dC2PGrsb+9xQulYz2ZHV9CoZOpTenZWR/AmbQ5Q # 6B4FztfsxIqSdgRSikdr9PWiE+RmyueX/oYjgCa7gUZ9c1BqfnHpu6xXGM1i8ONk # 9CozOne3OS21ZvusHAEYPzUzu8CgZoGIWL0mpAf5yWNIHDnAk8eMbQaUkMdb/on7 # I4prQQ+VIxKSA41+K3bk+b8= # SIG # End signature block |