Framework/Abstracts/ServicesSecurityStatus.ps1
Set-StrictMode -Version Latest class ServicesSecurityStatus: ADOSVTCommandBase { [SVTResourceResolver] $Resolver = $null; [bool] $IsPartialCommitScanActive = $false; [System.Diagnostics.Stopwatch] $StopWatch [Datetime] $ScanStart [Datetime] $ScanEnd [bool] $IsAIEnabled = $false; [bool] $IsBugLoggingEnabled = $false; [bool] $IsSarifEnabled = $false; $ActualResourcesPerRsrcType = @(); # Resources count based on resource type . This count is evaluated before comparison with resource tracker file. [bool] $IsControlFixCommand = $false; [string] $controlInternalId; [bool] $IsBatchScan=$false; ServicesSecurityStatus([string] $organizationName, [InvocationInfo] $invocationContext, [SVTResourceResolver] $resolver): Base($organizationName, $invocationContext) { if(-not $resolver) { throw [System.ArgumentException] ("The argument 'resolver' is null"); } $this.Resolver = $resolver; $this.Resolver.LoadResourcesForScan(); #If resource scan count is more than allowed foe scan (>1000) then stopping scan and returning. if (!$this.Resolver.SVTResources) { return; } $this.ActualResourcesPerRsrcType = $this.Resolver.SVTResources | Group-Object -Property ResourceType |select-object Name, Count $this.UsePartialCommits = $invocationContext.BoundParameters["UsePartialCommits"]; $this.IsBatchScan = $invocationContext.BoundParameters["BatchScan"]; #BaseLineControlFilter with control ids $this.UseBaselineControls = $invocationContext.BoundParameters["UseBaselineControls"]; $this.UsePreviewBaselineControls = $invocationContext.BoundParameters["UsePreviewBaselineControls"]; if ([RemoteReportHelper]::IsAIOrgTelemetryEnabled()) { $this.IsAIEnabled = $true; } if($invocationContext.BoundParameters["AutoBugLog"] -or $invocationContext.BoundParameters["AutoCloseBugs"]){ $this.IsBugLoggingEnabled = $true; } if($invocationContext.BoundParameters["ALTControlEvaluationMethod"]) { [IdentityHelpers]::ALTControlEvaluationMethod = $invocationContext.BoundParameters["ALTControlEvaluationMethod"] } if($invocationContext.BoundParameters["GenerateSarifLogs"]){ $this.IsSarifEnabled = $true; } [PartialScanManager]::ClearInstance(); $this.BaselineFilterCheck(); #get all controls covered under Set-AzSKADOBaselineConfigurations if($invocationContext.MyCommand.Name -eq "Set-AzSKADOBaselineConfigurations"){ $this.BaselineConfigurationsCheck() } $this.UsePartialCommitsCheck(); } #Contructor for Set-AzSKADOSecurityStatus command ServicesSecurityStatus([string] $organizationName, [string] $projectName, [InvocationInfo] $invocationContext, [SVTResourceResolver] $resolver, [string] $ControlId): Base($organizationName, $invocationContext) { $this.IsControlFixCommand = $true $this.FilterTags = "AutomatedFix" $this.MapTagsToControlIds(); if ($this.ControlIds.Count -gt 0) { $this.Resolver = $resolver; $this.Resolver.FetchControlFixBackupFile($organizationName, $projectName, $this.controlInternalId); if ([ControlHelper]::ControlFixBackup.Count -eq 0) { break; } $this.Resolver.LoadResourcesForScan(); if (!$this.Resolver.SVTResources) { return; } else { if (-not $invocationContext.BoundParameters["Force"]) { $ControlSettings = [ConfigurationManager]::LoadServerConfigFile("ControlSettings.json"); $backupLimit = $ControlSettings.AutomatedFix.BackupLimitInDays; $oldBackupResourcesFound = $false # [ControlHelper]::ControlFixBackup now has only relevant data based on this scan's paramaters foreach ($resource in [ControlHelper]::ControlFixBackup) { $dateDiff = New-TimeSpan -Start ([datetime]$resource.date) -End (GET-DATE) if($dateDiff.Days -gt $backupLimit) { $oldBackupResourcesFound = $true break; } } if ($oldBackupResourcesFound) { $this.PublishCustomMessage("`nOne or more resources have backup older than $($backupLimit) days. `nRun Gads with -PrepareForFix parameter to take backup again.`nOr use -Force in the Set-AzSKADOSecurityStatus command to proceed with the same backup.",[MessageType]::Warning); break; } } } $this.UsePartialCommits = $invocationContext.BoundParameters["UsePartialCommits"]; $this.UsePartialCommitsCheck(); } else { $this.PublishCustomMessage("`nControl $($ControlId) does not support automated fix.",[MessageType]::Warning); break; } } hidden [SVTEventContext[]] RunForAllResources([string] $methodNameToCall, [bool] $runNonAutomated, [PSObject] $resourcesList) { $ControlSettings = [ConfigurationManager]::LoadServerConfigFile("ControlSettings.json"); $scanSource = [AzSKSettings]::GetInstance().GetScanSource(); $cleanProcessedResources = $false if ($Env:AzSKADOUPCSimulate -eq $true) { $ControlSettings.PartialScan.LocalScanUpdateFrequency = $Env:AzSKADOLocalScanUpdateFrequency $ControlSettings.PartialScan.DurableScanUpdateFrequency = $Env:AzSKADODurableScanUpdateFrequency } ` if([Helpers]::CheckMember($ControlSettings, "CleanProcessedResources") -and $ControlSettings.CleanProcessedResources){ $cleanProcessedResources = $true } if ([string]::IsNullOrWhiteSpace($methodNameToCall)) { throw [System.ArgumentException] ("The argument 'methodNameToCall' is null. Pass the reference of method to call. e.g.: [YourClass]::new().YourMethod"); } $this.Severity = $this.ConvertToStringArray($this.Severity) # to handle when no severity is passed in command if($this.Severity) { $this.Severity = [ControlHelper]::CheckValidSeverities($this.Severity); } [SVTEventContext[]] $result = @(); if(($resourcesList | Measure-Object).Count -eq 0) { $this.PublishCustomMessage("No security controls/resources match the input criteria specified. `nPlease rerun the command using a different set of criteria."); return $result; } $this.PublishCustomMessage("Number of resources: $($this.resolver.SVTResourcesFoundCount)"); $automatedResources = @(); $automatedResources += ($resourcesList | Where-Object { $_.ResourceTypeMapping }); <# Resources skipped from scan using excludeResourceName parameter $ExcludedResources=$this.resolver.ExcludedResources ; if(($this.resolver.ExcludeResourceNames| Measure-Object).Count -gt 0) { $this.PublishCustomMessage("One or more resources/resource groups will be excluded from the scan based on exclude flags.") if(-not [string]::IsNullOrEmpty($this.resolver.ExcludeResourceGroupWarningMessage)) { $this.PublishCustomMessage("$($this.resolver.ExcludeResourceGroupWarningMessage)",[MessageType]::Warning) } if(-not [string]::IsNullOrEmpty($this.resolver.ExcludeResourceWarningMessage)) { $this.PublishCustomMessage("$($this.resolver.ExcludeResourceWarningMessage)",[MessageType]::Warning) } $this.PublishCustomMessage("Summary of exclusions: "); $this.PublishCustomMessage(" Resources excluded: $(($ExcludedResources | Measure-Object).Count)(includes RGs,resourcetypenames and explicit exclusions).", [MessageType]::Info); $this.PublishCustomMessage("For a detailed list of excluded resources, see 'ExcludedResources-$($this.RunIdentifier).txt' in the output log folder.") $this.ReportExcludedResources($this.resolver); } #> if($runNonAutomated) { $this.ReportNonAutomatedResources(); } #Begin-perf-optimize for ControlIds parameter #If controlIds are specified filter only to applicable resources #Filter resources based control tags like OwnerAccess, GraphAccess,RBAC, Authz, SOX etc $this.MapTagsToControlIds(); #Filter automated resources based on control ids $automatedResources = $this.MapControlsToResourceTypes($automatedResources) #End-perf-optimize $this.PublishCustomMessage("`nNumber of resources for which security controls will be evaluated: $($automatedResources.Count)",[MessageType]::Info); if ($this.IsAIEnabled) { $this.StopWatch = New-Object System.Diagnostics.Stopwatch #Send Telemetry for actual resource count. This is being done to monitor perf issues in ADOScanner internally if ($this.UsePartialCommits) { $resourceTypeCountHT = @{} foreach ($resType in $this.ActualResourcesPerRsrcType) { $resourceTypeCountHT["$($resType.Name)"] = "$($resType.Count)" } [AIOrgTelemetryHelper]::TrackCommandExecution("Actual Resources Count", @{"RunIdentifier" = $this.RunIdentifier}, $resourceTypeCountHT, $this.InvocationContext); } #Send Telemetry for target resource count (after partial commits has been checked). This is being done to monitor perf issues in ADOScanner internally $resourceTypeCount =$automatedResources | Group-Object -Property ResourceType |select-object Name, Count $resourceTypeCountHT = @{} foreach ($resType in $resourceTypeCount) { $resourceTypeCountHT["$($resType.Name)"] = "$($resType.Count)" } $memoryUsage = [System.Diagnostics.Process]::GetCurrentProcess().PrivateMemorySize64 / [Math]::Pow(10,6) $resourceTypeCountHT += @{MemoryUsageInMB = $memoryUsage} [AIOrgTelemetryHelper]::TrackCommandExecution("Target Resources Count", @{"RunIdentifier" = $this.RunIdentifier}, $resourceTypeCountHT, $this.InvocationContext); } $totalResources = $automatedResources.Count; [int] $currentCount = 0; $childResources = @(); #Declaring null object variable here, will initialize latter. $svtObject = $null; #Declaring $resourceTypesForCommonSVT to store resource types which uses common file. $resourceTypesForCommonSVT = ""; if ([Helpers]::CheckMember($ControlSettings, "ResourceTypesForCommonSVT")) { $resourceTypesForCommonSVT = $ControlSettings.ResourceTypesForCommonSVT } if($this.invocationContext.MyCommand.Name -eq "Set-AzSKADOSecurityStatus") { #Send resource count to usage telemetry in case of bulk remediation $this.PublishAzSKRootEvent([SVTEvent]::ResourceCount,$totalResources); } $automatedResources | ForEach-Object { $exceptionMessage = "Exception for resource: [ResourceType: $($_.ResourceTypeMapping.ResourceTypeName)] [ResourceGroupName: $($_.ResourceGroupName)] [ResourceName: $($_.ResourceName)]" try { if ($this.IsAIEnabled) { $this.ScanStart = [DateTime]::UtcNow $this.StopWatch.Restart() } $currentCount += 1; if($totalResources -gt 1) { $this.PublishCustomMessage(" `r`nChecking resource [$currentCount/$totalResources] "); } #Getting class name here from resourcetypemapping $svtClassName = $_.ResourceTypeMapping.ClassName; #Update resource scan retry count in scan snapshot in storage if user partial commit switch is on if($this.UsePartialCommits) { $this.UpdateRetryCountForPartialScan(); } try { $extensionSVTClassName = $svtClassName + "Ext"; $extensionSVTClassFilePath = $null #Check if the extended class of this type is already loaded? if(-not ($extensionSVTClassName -as [type])) { #Check if we know from a previous attempt that this 'type' has not been extended. if ([ConfigurationHelper]::NotExtendedTypes.containsKey($svtClassName)) { $extensionSVTClassFilePath = $null } else { $extensionSVTClassFilePath = [ConfigurationManager]::LoadExtensionFile($svtClassName); if ([string]::IsNullOrEmpty($extensionSVTClassFilePath)) { [ConfigurationHelper]::NotExtendedTypes["$svtClassName"] = $true } } #If $extensionSVTClassFilePath is null => use the built-in type from our module. if([string]::IsNullOrWhiteSpace($extensionSVTClassFilePath)) { #Check if $svtClassName is not common class then create object. #Check if $svtClassName is common class and objec of this class is not already created then on create new object. if ($svtClassName -ne "CommonSVTControls" -or ($svtClassName -eq "CommonSVTControls" -and (!$svtObject -or $svtObject.ResourceContext.ResourceTypeName -notin $resourceTypesForCommonSVT))) { $svtObject = New-Object -TypeName $svtClassName -ArgumentList $this.OrganizationContext.OrganizationName, $_ } else { $svtObject.ResourceId = $_.ResourceId; $svtObject.ResourceContext = [ResourceContext]@{ ResourceGroupName = $_.ResourceGroupName; ResourceName = $_.ResourceName; ResourceType = $_.ResourceTypeMapping.ResourceType; ResourceTypeName = $_.ResourceTypeMapping.ResourceTypeName; ResourceId = $_.ResourceId ResourceDetails = $_.ResourceDetails }; $svtObject.ControlStateExt.resourceName = $_.ResourceName; } } else #Use extended type. { # file has to be loaded here due to scope contraint Write-Warning "########## Loading extended type [$extensionSVTClassName] into memory ##########" . $extensionSVTClassFilePath $svtObject = New-Object -TypeName $extensionSVTClassName -ArgumentList $this.OrganizationContext.OrganizationName, $_ } } else { # Extended type is already loaded. Create an instance of that type. $svtObject = New-Object -TypeName $extensionSVTClassName -ArgumentList $this.OrganizationContext.OrganizationName, $_ } } catch { $this.PublishCustomMessage($exceptionMessage); # Unwrapping the first layer of exception which is added by New-Object function $this.CommandError($_.Exception.InnerException.ErrorRecord); } [SVTEventContext[]] $currentResourceResults = @(); if($svtObject) { $svtObject.RunningLatestPSModule = $this.RunningLatestPSModule; $this.SetSVTBaseProperties($svtObject); $childResources += $svtObject.ChildSvtObjects; $currentResourceResults += $svtObject.$methodNameToCall(); $result += $currentResourceResults; } if([Organization]::InstalledextensionInfo -or [Organization]::SharedextensionInfo -or [Organization]::AutoInjectedExtensionInfo) { # Default value if property 'ExtensionsLastUpdatedInYears' not exist in ControlSettings $years = 2 # Fetching property 'ExtensionsLastUpdatedInYears' from ControlSettings to print in csv column. if([Helpers]::CheckMember($svtObject.ControlSettings, "Organization.ExtensionsLastUpdatedInYears")) { $years = $svtObject.ControlSettings.Organization.ExtensionsLastUpdatedInYears } if ([Organization]::InstalledextensionInfo) { $folderpath=([WriteFolderPath]::GetInstance().FolderPath) + "\$($_.ResourceName)"+"_InstalledExtensionInfo.csv"; $MaxScore = [Organization]::InstalledextensionInfo[0].MaxScore [Organization]::InstalledextensionInfo | Select-Object extensionName,publisherId,KnownPublisher,publisherName,version,@{Name = "Too Old (>$($years)year(s))"; Expression = { $_.TooOld } },@{Name = "LastPublished"; Expression = { $_.lastPublished} },@{Name = "Sensitive Permissions"; Expression = { $_.SensitivePermissions} },@{Name = "NonProd (ExtensionName)"; Expression = { $_.NonProdByName}},@{Name = "NonProd (GalleryFlags) "; Expression = { $_.Preview }},TopPublisher,PrivateVisibility,NoOfInstalls,MarketPlaceAverageRating,@{Name = "Score (Out of $($MaxScore))"; Expression = { $_.Score } } | Export-Csv -Path $folderpath -NoTypeInformation -encoding utf8 #The NoTypeInformation parameter removes the #TYPE information header from the CSV output [Organization]::InstalledExtensionInfo = @() # Clearing the static variable value so that extensioninfo.csv file gets generated only once and when computed during the installed extension control } if ([Organization]::SharedextensionInfo) { $folderpath=([WriteFolderPath]::GetInstance().FolderPath) + "\$($_.ResourceName)"+"_SharedExtensionInfo.csv"; $MaxScore = [Organization]::SharedextensionInfo[0].MaxScore [Organization]::SharedextensionInfo | Select-Object extensionName,publisherId,KnownPublisher,publisherName,version,@{Name = "Too Old (>$($years)year(s))"; Expression = { $_.TooOld } },@{Name = "LastPublished"; Expression = { $_.lastPublished} },@{Name = "Sensitive Permissions"; Expression = { $_.SensitivePermissions} },@{Name = "NonProd (ExtensionName)"; Expression = { $_.NonProdByName}},@{Name = "NonProd (GalleryFlags) "; Expression = { $_.Preview }},TopPublisher,PrivateVisibility,NoOfInstalls,MarketPlaceAverageRating,@{Name = "Score (Out of $($MaxScore))"; Expression = { $_.Score } } | Export-Csv -Path $folderpath -NoTypeInformation -encoding utf8 #The NoTypeInformation parameter removes the #TYPE information header from the CSV output [Organization]::SharedextensionInfo = @() # Clearing the static variable value so that extensioninfo.csv file gets generated only once and when computed during the installed extension control } if ([Organization]::AutoInjectedExtensionInfo) { $folderpath=([WriteFolderPath]::GetInstance().FolderPath) + "\$($_.ResourceName)"+"_AutoInjectedExtensionInfo.csv"; $MaxScore = [Organization]::AutoInjectedExtensionInfo[0].MaxScore [Organization]::AutoInjectedExtensionInfo | Select-Object extensionName,publisherId,KnownPublisher,publisherName,version,@{Name = "Too Old (>$($years)year(s))"; Expression = { $_.TooOld } },@{Name = "LastPublished"; Expression = { $_.lastPublished} },@{Name = "Sensitive Permissions"; Expression = { $_.SensitivePermissions} },@{Name = "NonProd (ExtensionName)"; Expression = { $_.NonProdByName}},@{Name = "NonProd (GalleryFlags) "; Expression = { $_.Preview }},TopPublisher,PrivateVisibility,NoOfInstalls,MarketPlaceAverageRating,@{Name = "Score (Out of $($MaxScore))"; Expression = { $_.Score } } | Export-Csv -Path $folderpath -NoTypeInformation -encoding utf8 #The NoTypeInformation parameter removes the #TYPE information header from the CSV output [Organization]::AutoInjectedExtensionInfo = @() # Clearing the static variable value so that extensioninfo.csv file gets generated only once and when computed during the installed extension control } } $memoryUsage = 0 if(($result | Measure-Object).Count -gt 0 -and $this.UsePartialCommits) { $updateSucceeded = $false if ([system.String]::IsNullOrEmpty($scanSource) -or $scanSource -eq "SDL") { if($currentCount % $ControlSettings.PartialScan.LocalScanUpdateFrequency -eq 0 -or $currentCount -eq $totalResources) { # Update local resource tracker file $this.UpdatePartialCommitFile($false, $result) #If this is a batch scan, update the inventory count and add to tracker if($this.IsBatchScan) { $this.UpdateBatchScanCount($currentCount,$totalResources); } $updateSucceeded = $true } } else{ if($currentCount % $ControlSettings.PartialScan.DurableScanUpdateFrequency -eq 0 -or $currentCount -eq $totalResources) { # Update durable resource tracker file $this.UpdatePartialCommitFile($true, $result) $updateSucceeded = $true } } if ($updateSucceeded) { [SVTEventContext[]] $result = @(); [System.GC]::Collect(); $memoryUsage = [System.Diagnostics.Process]::GetCurrentProcess().PrivateMemorySize64 / [Math]::Pow(10,6) } } #Send Telemetry for scan time taken for a resource. This is being done to monitor perf issues in ADOScanner internally if ($this.IsAIEnabled) { $this.StopWatch.Stop() $this.ScanEnd = [DateTime]::UtcNow $properties = @{ TimeTakenInMs = $this.StopWatch.ElapsedMilliseconds; ResourceCount = "$currentCount/$totalResources"; ResourceName = $svtObject.ResourceContext.ResourceName; ResourceType = $svtObject.ResourceContext.ResourceType ; ScanStartDateTime = $this.ScanStart; ScanEndDateTime = $this.ScanEnd; RunIdentifier = $this.RunIdentifier; } if ($memoryUsage -gt 0) { $properties += @{MemoryUsageInMB = $memoryUsage;} } [AIOrgTelemetryHelper]::PublishEvent( "Resource Scan Completed",$properties, @{}) } if ($cleanProcessedResources) { $resourcesList.remove($_); } } catch { $this.PublishCustomMessage($exceptionMessage); $this.CommandError($_); } } if(($childResources | Measure-Object).Count -gt 0) { try { [SVTEventContext[]] $childResourceResults = @(); $temp= $childResources |Sort-Object -Property @{Expression={$_.ResourceId}} -Unique $temp| ForEach-Object { $_.RunningLatestPSModule = $this.RunningLatestPSModule $this.SetSVTBaseProperties($_) $childResourceResults += $_.$methodNameToCall(); } $result += $childResourceResults; } catch { $this.PublishCustomMessage($_); } } return $result; } hidden [SVTEventContext[]] RunAllControls() { return $this.RunForAllResources("EvaluateAllControls",$true,$this.Resolver.SVTResources) } hidden [void] ReportNonAutomatedResources() { $nonAutomatedResources = @(); $nonAutomatedResources += ($this.Resolver.SVTResources | Where-Object { $null -eq $_.ResourceTypeMapping }); if(($nonAutomatedResources|Measure-Object).Count -gt 0) { $this.PublishCustomMessage("Number of resources for which security controls will NOT be evaluated: $($nonAutomatedResources.Count)", [MessageType]::Warning); $nonAutomatedResTypes = [array] ($nonAutomatedResources | Select-Object -Property ResourceType -Unique); $this.PublishCustomMessage([MessageData]::new("Security controls are yet to be automated for the following service types: ", $nonAutomatedResTypes)); $this.PublishAzSKRootEvent([AzSKRootEvent]::UnsupportedResources, $nonAutomatedResources); } } #Rescan controls post attestation hidden [SVTEventContext[]] ScanAttestedControls() { [ControlStateExtension] $ControlStateExt = [ControlStateExtension]::new($this.OrganizationContext, $this.InvocationContext); $ControlStateExt.UniqueRunId = $this.ControlStateExt.UniqueRunId; $ControlStateExt.Initialize($false); #$ControlStateExt.ComputeControlStateIndexer(); [PSObject] $ControlStateIndexer = $null; foreach ($items in $this.Resolver.SVTResources) { $resourceType = $null; $projectName = $null; if ($items.ResourceType -ne "ADO.Organization") { if ($items.ResourceType -eq "ADO.Project") { $projectName = $items.ResourceName $resourceType = "Project"; } else { $projectName = $items.ResourceGroupName $resourceType = $items.ResourceType } } else { $resourceType = "Organization"; } $ControlStateIndexer += $ControlStateExt.RescanComputeControlStateIndexer($projectName, $resourceType); } $ControlStateIndexer = $ControlStateIndexer | Select-Object * -Unique $resourcesAttestedinCurrentScan = @() if(($null -ne $ControlStateIndexer) -and ([Helpers]::CheckMember($ControlStateIndexer, "ResourceId"))) { $resourcesAttestedinCurrentScan = $this.Resolver.SVTResources | Where-Object {$ControlStateIndexer.ResourceId -contains $_.ResourceId} } return $this.RunForAllResources("RescanAndPostAttestationData",$false,$resourcesAttestedinCurrentScan) } #BaseLine Control Filter Function [void] BaselineFilterCheck() { #Check if use baseline or preview baseline flag is passed as parameter if($this.UseBaselineControls -or $this.UsePreviewBaselineControls) { $ResourcesWithBaselineFilter =@() #Load ControlSetting file $ControlSettings = [ConfigurationManager]::LoadServerConfigFile("ControlSettings.json"); $baselineControlsDetails = $ControlSettings.BaselineControls #if baselineControls switch is available and baseline controls available in settings if ($null -ne $baselineControlsDetails -and ($baselineControlsDetails.ResourceTypeControlIdMappingList | Measure-Object).Count -gt 0 -and $this.UseBaselineControls) { #Get resource type and control ids mapping from controlsetting object #$this.PublishCustomMessage("Running cmdlet with baseline resource types and controls.", [MessageType]::Warning); $baselineResourceTypes = $baselineControlsDetails.ResourceTypeControlIdMappingList | Select-Object ResourceType | Foreach-Object {$_.ResourceType} #Filter SVT resources based on baseline resource types $ResourcesWithBaselineFilter += $this.Resolver.SVTResources | Where-Object {$null -ne $_.ResourceTypeMapping -and $_.ResourceTypeMapping.ResourceTypeName -in $baselineResourceTypes } #Get the list of control ids $controlIds = $baselineControlsDetails.ResourceTypeControlIdMappingList | Select-Object ControlIds | ForEach-Object { $_.ControlIds } $BaselineControlIds = [system.String]::Join(",",$controlIds); if(-not [system.String]::IsNullOrEmpty($BaselineControlIds)) { #Assign preview control list to ControlIds filter parameter. This controls gets filtered during scan. $this.ControlIds = $controlIds; } } #If baseline switch is passed and there is no baseline control list present then throw exception elseif (($baselineControlsDetails.ResourceTypeControlIdMappingList | Measure-Object).Count -eq 0 -and $this.UseBaselineControls) { throw ([SuppressedException]::new(("There are no baseline controls defined for your org. No controls will be scanned."), [SuppressedExceptionType]::Generic)) } #Preview Baseline Controls $previewBaselineControlsDetails = $null #if use preview baseline switch is passed and preview baseline list property present if($this.UsePreviewBaselineControls -and [Helpers]::CheckMember($ControlSettings,"PreviewBaselineControls")) { $previewBaselineControlsDetails = $ControlSettings.PreviewBaselineControls #if preview baseline list is defined in settings if ($null -ne $previewBaselineControlsDetails -and ($previewBaselineControlsDetails.ResourceTypeControlIdMappingList | Measure-Object).Count -gt 0 ) { $previewBaselineResourceTypes = $previewBaselineControlsDetails.ResourceTypeControlIdMappingList | Select-Object ResourceType | Foreach-Object {$_.ResourceType} #Filter SVT resources based on preview baseline baseline resource types $BaselineResourceList = @() if(($ResourcesWithBaselineFilter | Measure-Object).Count -gt 0) { $BaselineResourceList += $ResourcesWithBaselineFilter | Foreach-Object { $_.ResourceId} } $ResourcesWithBaselineFilter += $this.Resolver.SVTResources | Where-Object {$null -ne $_.ResourceTypeMapping -and $_.ResourceTypeMapping.ResourceTypeName -in $previewBaselineResourceTypes -and $_.ResourceId -notin $BaselineResourceList } #Get the list of preview control ids $controlIds = $previewBaselineControlsDetails.ResourceTypeControlIdMappingList | Select-Object ControlIds | ForEach-Object { $_.ControlIds } $previewBaselineControlIds = [system.String]::Join(",",$controlIds); if(-not [system.String]::IsNullOrEmpty($previewBaselineControlIds)) { # Assign preview control list to ControlIds filter parameter. This controls gets filtered during scan. $this.ControlIds += $controlIds; } } #If preview baseline switch is passed and there is no baseline control list present then throw exception elseif (($previewBaselineControlsDetails.ResourceTypeControlIdMappingList | Measure-Object).Count -eq 0 -and $this.UsePreviewBaselineControls) { if(($baselineControlsDetails.ResourceTypeControlIdMappingList | Measure-Object).Count -eq 0 -and $this.UseBaselineControls) { throw ([SuppressedException]::new(("There are no baseline and preview-baseline controls defined for this policy. No controls will be scanned."), [SuppressedExceptionType]::Generic)) } if(-not ($this.UseBaselineControls)) { throw ([SuppressedException]::new(("There are no preview-baseline controls defined for your org. No controls will be scanned."), [SuppressedExceptionType]::Generic)) } } } #Assign baseline filtered resources to SVTResources list (resource list to be scanned) if(($ResourcesWithBaselineFilter | Measure-Object).Count -gt 0) { $this.Resolver.SVTResources = [SVTResource[]] $ResourcesWithBaselineFilter } } } [void] BaselineConfigurationsCheck(){ $ResourcesWithBaselineConfigFilter =@() $ControlSettings = [ConfigurationManager]::LoadServerConfigFile("ControlSettings.json"); $baselineConfigControlsDetails = $ControlSettings.BaselineConfigurationsControls; $baselineResourceTypes = $baselineConfigControlsDetails.ResourceTypeControlIdMappingList | Select-Object ResourceType | Foreach-Object {$_.ResourceType} $ResourcesWithBaselineConfigFilter += $this.Resolver.SVTResources | Where-Object {$null -ne $_.ResourceTypeMapping -and $_.ResourceTypeMapping.ResourceTypeName -in $baselineResourceTypes } $controlIds = $baselineConfigControlsDetails.ResourceTypeControlIdMappingList | Select-Object ControlIds | ForEach-Object { $_.ControlIds } $BaselineControlIds = [system.String]::Join(",",$controlIds); if(-not [system.String]::IsNullOrEmpty($BaselineControlIds)) { $this.ControlIds = $controlIds; } if(($ResourcesWithBaselineConfigFilter | Measure-Object).Count -gt 0) { $this.Resolver.SVTResources = [SVTResource[]] $ResourcesWithBaselineConfigFilter } } [void] UpdateRetryCountForPartialScan() { [PartialScanManager] $partialScanMngr = [PartialScanManager]::GetInstance(); #If Scan source is in supported sources or UsePartialCommits switch is available if ($this.UsePartialCommits) { $partialScanMngr.UpdateResourceScanRetryCount($_.ResourceId); } } [void] UpdateBatchScanCount($currentCount,$totalResources) { $ControlSettings = [ConfigurationManager]::LoadServerConfigFile("ControlSettings.json"); if($PSCmdlet.MyInvocation.BoundParameters.ContainsKey("BatchScanMultipleProjects")){ [BatchScanManagerForMultipleProjects] $batchScanMngr = [BatchScanManagerForMultipleProjects]:: GetInstance(); } else { [BatchScanManager] $batchScanMngr = [BatchScanManager]:: GetInstance(); } $batchStatus = $batchScanMngr.GetBatchStatus(); if($currentCount % $ControlSettings.PartialScan.LocalScanUpdateFrequency -eq 0){ $batchStatus.ResourceCount += $ControlSettings.PartialScan.LocalScanUpdateFrequency; } elseif($currentCount -eq $totalResources){ $batchStatus.ResourceCount += ($totalResources % $ControlSettings.PartialScan.LocalScanUpdateFrequency ); } $batchScanMngr.BatchScanTrackerObj = $batchStatus; $batchScanMngr.WriteToBatchTrackerFile(); } [void] UpdatePartialCommitFile($isDurableStorageUpdate , $result) { [PartialScanManager] $partialScanMngr = [PartialScanManager]::GetInstance(); #If Scan source is in supported sources or UsePartialCommits switch is available if ($isDurableStorageUpdate) { $partialScanMngr.WriteToDurableStorage(); } else { if($this.invocationContext.BoundParameters["PrepareForControlFix"]){ $partialScanMngr.WriteControlFixDataObject($result); } $partialScanMngr.WriteToResourceTrackerFile(); } # write to csv after every partial commit $partialScanMngr.WriteToCSV($result, [FileOutputBase]::CSVFilePath); # append summary counts $partialScanMngr.CollateSummaryData($result); # append summary counts for bug logging & append control results with bug logging data if($this.IsBugLoggingEnabled){ if($this.invocationContext.BoundParameters["AutoBugLog"]){ $partialScanMngr.CollateBugSummaryData($result); } #Closes bugs after every partial commit $AutoClose=[AutoCloseBugManager]::new($this.OrganizationContext.OrganizationName); $AutoClose.AutoCloseBug($result) $bugsClosed=[AutoCloseBugManager]::ClosedBugs #Collects closed bugs information in partialScanManager class $partialScanMngr.CollateClosedBugSummaryData($bugsClosed) #Sends closed bugs information to Log Analytics after every partial commit. if($bugsClosed){ $laInstance= [LogAnalyticsOutput]::Instance $laInstance.WriteControlResult($bugsClosed) } } #sarif information. Save in ControlResultsWithSarifSummary only if controls not available in ControlResultsWithBugSummary if($this.IsSarifEnabled -and !$this.invocationContext.BoundParameters["AutoBugLog"]){ $partialScanMngr.CollateSarifData($result); } } [void] UsePartialCommitsCheck() { #If Scan source is in supported sources or UsePartialCommits switch is available if ($this.UsePartialCommits) { #Load ControlSetting Resource Types and Filter resources if($this.CentralStorageAccount){ [PartialScanManager] $partialScanMngr = [PartialScanManager]::GetInstance($this.CentralStorageAccount, $this.OrganizationContext.OrganizationName); } else{ [PartialScanManager] $partialScanMngr = [PartialScanManager]::GetInstance(); } #$this.PublishCustomMessage("Running cmdlet under transactional mode. This will scan resources and store intermittent scan progress to Storage. It resume scan in next run if something breaks inbetween.", [MessageType]::Warning); #Validate if active resources list already available in store #If list not available in store. Get resources filtered by baseline resource types and store it storage $nonScannedResourcesList = @(); #Sending $this.isControlFixCommand as true in case set-azskadosecuritystatus command is used in order to store RTF in separate folder, so that it does not interfere with GADS command if(($partialScanMngr.IsPartialScanInProgress($this.OrganizationContext.OrganizationName, $this.IsControlFixCommand) -eq [ActiveStatus]::Yes) ) { $this.IsPartialCommitScanActive = $true; $allResourcesList = $partialScanMngr.GetAllListedResources() # Get list of non-scanned active resources Write-Host "Finding unscanned resources" -ForegroundColor Yellow $nonScannedResourcesList = $partialScanMngr.GetNonScannedResources(); $this.PublishCustomMessage("Resuming scan from last commit. $(($nonScannedResourcesList | Measure-Object).Count) out of $(($allResourcesList | Measure-Object).Count) resources will be scanned.", [MessageType]::Warning); $nonScannedResourceIdList = $nonScannedResourcesList | Select-Object Id | ForEach-Object { $_.Id} #Filter SVT resources based on master resources list available and scan completed #Commenting telemtry here to include PartialScanIdentifier #[AIOrgTelemetryHelper]::PublishEvent( "Partial Commit Details", @{"TotalSVTResources"= $($this.Resolver.SVTResources | Where-Object { $_.ResourceTypeMapping } | Measure-Object).Count;"UnscannedResource"=$(($nonScannedResourcesList | Measure-Object).Count); "ResourceToBeScanned" = ($this.Resolver.SVTResources | Where-Object {$_.ResourceId -in $nonScannedResourceIdList } | Measure-Object).Count;},$null) $this.Resolver.SVTResources = $this.Resolver.SVTResources | Where-Object {$_.ResourceId -in $nonScannedResourceIdList } } else{ $this.IsPartialCommitScanActive = $false; [System.Collections.Generic.List[PSCustomObject]] $resourceLists=@() $progressCount=1 foreach ($svtResource in $this.Resolver.SVTResources) { if($null -ne $svtResource.ResourceTypeMapping){ $resourceList=[PSCustomObject]@{ ResourceId = $svtResource.ResourceId ResourceName=$svtResource.ResourceName ResourceGroupName = $svtResource.ResourceGroupName ResourceType = $svtResource.ResourceType #ResourceDetails=$svtResource.ResourceDetails } $resourceLists.Add($resourceList) if ($progressCount%100 -eq 0) { Write-Progress -Activity "Processed $($progressCount) of $($this.Resolver.SVTResources.Count) untracked resources " -Status "Progress: " -PercentComplete ($progressCount / $this.Resolver.SVTResources.Count * 100) } $progressCount++; } } Write-Progress -Activity "Processed all untracked resources" -Status "Ready" -Completed #$resourceIdList=@() #$resourceIdList += $this.Resolver.SVTResources| Where-Object {$null -ne $_.ResourceTypeMapping} | Select-Object ResourceId, ResourceName, ResourceDetails | ForEach-Object { $_.ResourceId, $_.ResourceName, $_.ResourceDetails } $partialScanMngr.CreateResourceMasterList($resourceLists); #This should fetch full list of resources to be scanned Write-Host "Finding unscanned resources" -ForegroundColor Yellow $nonScannedResourcesList = $partialScanMngr.GetNonScannedResources(); } #Set unique partial scan identifier (used for correlating events in AI when partial scan resumes.) #ADOTODO: Move '12' to Constants.ps1 later. $this.PartialScanIdentifier = [Helpers]::ComputeHashShort($partialScanMngr.ResourceScanTrackerObj.Id,12) #Telemetry with addition for Subscription Id, PartialScanIdentifier and correction in count of resources #Need optimization for calcuations done for total resources. try{ $CompletedResources = 0; $IncompleteScans = 0; $InErrorResources = 0; $ScanResourcesList = $partialScanMngr.GetAllListedResources() $progressCount=1 $ScanResourcesList | Group-Object -Property State | Select-Object Name,Count | foreach { if($_.Name -eq "COMP") { $CompletedResources = $_.Count } elseif ($_.Name -eq "INIT") { $IncompleteScans = $_.Count } elseif ($_.Name -eq "ERR") { $InErrorResources = $_.Count } if ($progressCount%100 -eq 0) { Write-Progress -Activity "Computed status of $($progressCount) of $($ScanResourcesList.Count) untracked resources " -Status "Progress: " -PercentComplete ($progressCount / $ScanResourcesList.Count * 100) } $progressCount++; } Write-Progress -Activity "Computed status of all untracked resources" -Status "Ready" -Completed [AIOrgTelemetryHelper]::PublishEvent( "Partial Commit Details",@{"TotalSVTResources"= $($ScanResourcesList |Measure-Object).Count;"ScanCompletedResourcesCount"=$CompletedResources; "NonScannedResourcesCount" = $IncompleteScans;"ErrorStateResourcesCount"= $InErrorResources;"OrganizationName"=$this.OrganizationContext.OrganizationName;"PartialScanIdentifier"=$this.PartialScanIdentifier;}, $null) } catch{ #Continue exexution if telemetry is not sent } } } #Get list of controlIds based control tags like OwnerAccess, GraphAccess,RBAC, Authz, SOX etc. [void] MapTagsToControlIds() { #Check if filtertags or exclude filter tags parameter is passed from user then get mapped control ids if(-not [string]::IsNullOrEmpty($this.FilterTags) ) #-or -not [string]::IsNullOrEmpty($this.ExcludeTags) { $resourcetypes = @() $controlList = @() #Get list of all supported resource Types $resourcetypes += ([SVTMapping]::AzSKADOResourceMapping | Sort-Object ResourceTypeName | Select-Object JsonFileName ) $resourcetypes | ForEach-Object{ #Fetch control json for all resource type and collect all control jsons $controlJson = [ConfigurationManager]::GetSVTConfig($_.JsonFileName); if ([Helpers]::CheckMember($controlJson, "Controls")) { $controlList += $controlJson.Controls | Where-Object {$_.Enabled} } } #If FilterTags are specified, limit the candidate set to matching controls if (-not [string]::IsNullOrEmpty($this.FilterTags)) { $filterTagList = $this.ConvertToStringArray($this.FilterTags) $controlIdsWithFilterTagList = @() #Look at each candidate control's tags and see if there's a match in FilterTags $filterTagList | ForEach-Object { $tagName = $_ $controlIdsWithFilterTagList += $controlList | Where-Object{ $tagName -in $_.Tags } | ForEach-Object{ $_.ControlId} } #Assign filtered control Id with tag name $this.ControlIds = @($controlIdsWithFilterTagList | Select-Object -Unique) #Need Control's internal id in case of Set-AzSKADOSecurityStatus command if ($this.IsControlFixCommand) { $inputControlId = $this.invocationContext.BoundParameters["ControlId"]; $this.ControlIds = $this.ControlIds | where-object {$_ -eq $inputControlId} $this.ControlInternalId = ($controlList | Where-Object { $inputControlId -contains $_.ControlId }| Select-Object Id -Unique).Id } } #********** Commentiing Exclude tags logic as this will not require perf optimization as excludeTags mostly will result in most of the resources # #If FilterTags are specified, limit the candidate set to matching controls # #Note: currently either includeTag or excludeTag will work at a time. Combined flag result will be overridden by excludeTags # if (-not [string]::IsNullOrEmpty($this.ExcludeTags)) # { # $excludeFilterTagList = $this.ConvertToStringArray($this.ExcludeTags) # $controlIdsWithFilterTagList = @() # #Look at each candidate control's tags and see if there's a match in FilterTags # $excludeFilterTagList | ForEach-Object { # $tagName = $_ # $controlIdsWithFilterTagList += $controlList | Where-Object{ $tagName -notin $_.Tags } | ForEach-Object{ $_.ControlId} # } # #Assign filtered control Id with tag name # $this.ControlIds = $controlIdsWithFilterTagList # } } } [PSObject] MapControlsToResourceTypes([PSObject] $automatedResources) { $allTargetControlIds = @($this.ControlIds) $allTargetControlIds += $this.ConvertToStringArray($this.ControlIdString) #Do this only for the actual controlIds case (not the Severity-Spec "Severity:High" case) if ($allTargetControlIds.Count -gt 0 ) { #Infer resource type names from control ids $allTargetResourceTypeNames = @($allTargetControlIds | ForEach-Object { ($_ -split '_')[1]}) $allTargetResourceTypeNamesUnique = @($allTargetResourceTypeNames | Sort-Object -Unique) #Match resources based on resource types. Here we have made exception for AzSKCfg to scan it every time and virtual network as its type name (VirtualNetwork) is different than controls type name (VNet) $automatedResources = @($automatedResources | Where-Object {$allTargetResourceTypeNamesUnique -contains $_.ResourceTypeMapping.ResourceTypeName -or $_.ResourceType -match 'AzSKCfg' -or ($_.ResourceTypeMapping.ResourceTypeName -match 'VirtualNetwork' -and $allTargetResourceTypeNamesUnique -contains "VNet")}) } return $automatedResources } [void] ReportExcludedResources($SVTResolver) { $excludedObj=New-Object -TypeName PSObject; $excludedObj | Add-Member -NotePropertyName ExcludedResources -NotePropertyValue $SVTResolver.ExcludedResources $excludedObj | Add-Member -NotePropertyName ExcludedResourceType -NotePropertyValue $SVTResolver.ExcludeResourceTypeName $excludedObj | Add-Member -NotePropertyName ExcludeResourceNames -NotePropertyValue $SVTResolver.ExcludeResourceNames $this.PublishAzSKRootEvent([AzSKRootEvent]::WriteExcludedResources,$excludedObj); } } # SIG # Begin signature block # MIInlQYJKoZIhvcNAQcCoIInhjCCJ4ICAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBj0JrlU10yf56J # sO+U/vbJJlAjwD3Pl9UYFWAQ+rNPDqCCDXYwggX0MIID3KADAgECAhMzAAADTrU8 # esGEb+srAAAAAANOMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjMwMzE2MTg0MzI5WhcNMjQwMzE0MTg0MzI5WjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQDdCKiNI6IBFWuvJUmf6WdOJqZmIwYs5G7AJD5UbcL6tsC+EBPDbr36pFGo1bsU # p53nRyFYnncoMg8FK0d8jLlw0lgexDDr7gicf2zOBFWqfv/nSLwzJFNP5W03DF/1 # 1oZ12rSFqGlm+O46cRjTDFBpMRCZZGddZlRBjivby0eI1VgTD1TvAdfBYQe82fhm # WQkYR/lWmAK+vW/1+bO7jHaxXTNCxLIBW07F8PBjUcwFxxyfbe2mHB4h1L4U0Ofa # +HX/aREQ7SqYZz59sXM2ySOfvYyIjnqSO80NGBaz5DvzIG88J0+BNhOu2jl6Dfcq # jYQs1H/PMSQIK6E7lXDXSpXzAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUnMc7Zn/ukKBsBiWkwdNfsN5pdwAw # RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW # MBQGA1UEBRMNMjMwMDEyKzUwMDUxNjAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci # tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j # b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG # CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu # Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0 # MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAD21v9pHoLdBSNlFAjmk # mx4XxOZAPsVxxXbDyQv1+kGDe9XpgBnT1lXnx7JDpFMKBwAyIwdInmvhK9pGBa31 # TyeL3p7R2s0L8SABPPRJHAEk4NHpBXxHjm4TKjezAbSqqbgsy10Y7KApy+9UrKa2 # kGmsuASsk95PVm5vem7OmTs42vm0BJUU+JPQLg8Y/sdj3TtSfLYYZAaJwTAIgi7d # hzn5hatLo7Dhz+4T+MrFd+6LUa2U3zr97QwzDthx+RP9/RZnur4inzSQsG5DCVIM # pA1l2NWEA3KAca0tI2l6hQNYsaKL1kefdfHCrPxEry8onJjyGGv9YKoLv6AOO7Oh # JEmbQlz/xksYG2N/JSOJ+QqYpGTEuYFYVWain7He6jgb41JbpOGKDdE/b+V2q/gX # UgFe2gdwTpCDsvh8SMRoq1/BNXcr7iTAU38Vgr83iVtPYmFhZOVM0ULp/kKTVoir # IpP2KCxT4OekOctt8grYnhJ16QMjmMv5o53hjNFXOxigkQWYzUO+6w50g0FAeFa8 # 5ugCCB6lXEk21FFB1FdIHpjSQf+LP/W2OV/HfhC3uTPgKbRtXo83TZYEudooyZ/A # Vu08sibZ3MkGOJORLERNwKm2G7oqdOv4Qj8Z0JrGgMzj46NFKAxkLSpE5oHQYP1H # tPx1lPfD7iNSbJsP6LiUHXH1MIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq # hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x # EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv # bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 # IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG # EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG # A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg # Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC # CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03 # a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr # rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg # OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy # 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9 # sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh # dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k # A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB # w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn # Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90 # lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w # ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o # ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD # VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa # BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny # bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG # AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t # L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV # HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3 # dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG # AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl # AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb # C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l # hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6 # I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0 # wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560 # STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam # ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa # J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah # XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA # 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt # Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr # /Xmfwb1tbWrJUnMTDXpQzTGCGXUwghlxAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw # EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN # aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp # Z25pbmcgUENBIDIwMTECEzMAAANOtTx6wYRv6ysAAAAAA04wDQYJYIZIAWUDBAIB # BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO # MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIC7f/umnfyves6c/IXNfT7Ly # RwAYau2s5IQ5H4lYJL4KMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A # cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB # BQAEggEAvS+f4IuDL+YFLm4rngSnGS9NBxcLWcEvCKGU/3i9/5NaKDlzqzfUI/7m # DTS57qmxGCDThd4Ln9HogLI5m4YHkE2M8s6k8FZxYFBwX8mIrnLFrgCCyDh4cdvg # CO7GBVpFv63CAbZJ9KPRJLKQFxSwV69nCZBxLITGcbjZW73VBoedHdE0+uUYFYkY # wWL8JIbW1rF5TSB0uCvWRvT7DQzVglzu4t627tC0qjhdPHpjXtomqYzbE8tF7piM # mDqtWMi2aaCjJQz8KBMRUdVQgMjM4cMRl4mszxnWdkeI//R6w61IUnRLiqWD9LO7 # 6ermYBbpkrPMWDOy2Eu2muzZLCLmmqGCFv8wghb7BgorBgEEAYI3AwMBMYIW6zCC # FucGCSqGSIb3DQEHAqCCFtgwghbUAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFQBgsq # hkiG9w0BCRABBKCCAT8EggE7MIIBNwIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl # AwQCAQUABCBjveEyP8pWFteja2klkhFqj6pG7dafEtxayT6SbjcEMgIGZK/5rBzW # GBIyMDIzMDcyMTEyNTE0NC40OFowBIACAfSggdCkgc0wgcoxCzAJBgNVBAYTAlVT # MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK # ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJTAjBgNVBAsTHE1pY3Jvc29mdCBBbWVy # aWNhIE9wZXJhdGlvbnMxJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNOOkQ2QkQtRTNF # Ny0xNjg1MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloIIR # VzCCBwwwggT0oAMCAQICEzMAAAHH+wCgSlvyJ9wAAQAAAccwDQYJKoZIhvcNAQEL # BQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT # B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UE # AxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwHhcNMjIxMTA0MTkwMTM1 # WhcNMjQwMjAyMTkwMTM1WjCByjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp # bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw # b3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEm # MCQGA1UECxMdVGhhbGVzIFRTUyBFU046RDZCRC1FM0U3LTE2ODUxJTAjBgNVBAMT # HE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEBAQUA # A4ICDwAwggIKAoICAQCvQtxW2dq00UwGtBO0b0/whIA/LIabE1+ETNo5WW3TzykF # QUAhqyY3946KMTpRxp/dzZtWc3/TaKHSyZKpiSbk/dnBTtlbbTZvpw8MmNdyuMmP # Sp+e5xwG0TdZTS9nwKJAPuqsrF4XxgE1xL49W2+yqF3lhboDCFaqGPDWZi4t60Xl # vpo+J//dHOXKobdJXtA+JIl6d2zuAbjflGzLUcnheerO04lHjUjSPcRDTkkwXlA1 # GLuRPq9dNP4wdWPbsVVDtt5/9T7YQBsWPZfYA5Zu+CVhpiczeb8j85YMdSAbDwoh # 2wOHdbV66ycXYPuh6caC1qGz5LUblSiV/kRKD/1n7fyuFDAuCiRjmTqnyTlqtha2 # zN0kromIhGXzjcfviTv5CqVPYtsBA+ryK9C/SB1yVbZom6fUqtb6/nZHe8AcI61t # SbG8PV40YeoaotqC2Wr1QVcpe5eepcmqu4JiZ/B0UwPRQ/qKLWUV14ovzs92N0DD # IKJVwISgue8PPK+M2PG2RN3PpHjIXU39fg9JAfgWWCyXIEheCBpKU+28+7EC25pz # 8hOPiTQhFKEaJgsEzYPDqh6ws6jF7Ts5Q876pdc5wkxUeETQyWGGfF83YHUlYU9b # BDqihaKoA5AOrNwPH7v2yHEDULHQrvR44GmUyiDbuBigukG/udHPi0eqhPK8DQID # AQABo4IBNjCCATIwHQYDVR0OBBYEFAVQ0t0cPsEAX9VT9f94QcuJRJIgMB8GA1Ud # IwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCGTmh0 # dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUyMFRp # bWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4wXAYI # KwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMv # TWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwGA1Ud # EwEB/wQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwgwDQYJKoZIhvcNAQELBQADggIB # ANDLlzyiA/TLzECwtVxrTvBbLWZP6epiAAWfPb3vaQ074onsATh/5JVu86eR5644 # +rfFz7pNLyDcW4opgTBiq+dEfFfny2OWxxmxl4qe7t8Y1SWk1P1s5AUdYAtRG6he # nxMseHGPc8Sr2PMVgE/Zg0wuiXvSiNjWqnN7ecwwl+l26t0EGlo4uUmZE1MuHF35 # EkYlBtjVcBzHqn8WKDCoFqxINTGn7TIU8QEH24ETcogsC2rp9zMangQx6ifpiaTI # IYC1cwoMVBCB0/8hN7tWCEBVs9NWU/eFjV0WBz63xgrahsVIVUqyWQBIBMMe6UIy # G35asiy6RyURQ/0NoyamrtLREs4MyJwjo+2qoY6F2dpGW0DR35Z/7S0+31JRW2s8 # nI7tYw8pvKQJFfOYcrTrOvSSfViJRg1cKw6BocXkiY7ZnBDnhQTUjnmONR2V3KPL # 9Q8mDFGb03Jd47tp1ivwrx/pDac8XS9aoUbt7DBoCXkKUp6vOyF+EHzO6NVHR3VF # rtnTWWddiFa4+pVlrIWXskevqLqG6GlToFDr9WBjRwGKSxfiY0z4hJjzVPVFi3t9 # YBM27/OSMg1zOKnNt+DlL7d8ICjyBUHr7oDkvS8GDf12wUhO/oxYm5DxlnLt/CUU # FkTh3kgVtG51qQ3AoZ3IsYzai1o2rvCbeS7vHjVQYCaQMIIHcTCCBVmgAwIBAgIT # MwAAABXF52ueAptJmQAAAAAAFTANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMC # VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV # BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJv # b3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMjEwOTMwMTgyMjI1WhcN # MzAwOTMwMTgzMjI1WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv # bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0 # aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCAiIw # DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAOThpkzntHIhC3miy9ckeb0O1YLT # /e6cBwfSqWxOdcjKNVf2AX9sSuDivbk+F2Az/1xPx2b3lVNxWuJ+Slr+uDZnhUYj # DLWNE893MsAQGOhgfWpSg0S3po5GawcU88V29YZQ3MFEyHFcUTE3oAo4bo3t1w/Y # JlN8OWECesSq/XJprx2rrPY2vjUmZNqYO7oaezOtgFt+jBAcnVL+tuhiJdxqD89d # 9P6OU8/W7IVWTe/dvI2k45GPsjksUZzpcGkNyjYtcI4xyDUoveO0hyTD4MmPfrVU # j9z6BVWYbWg7mka97aSueik3rMvrg0XnRm7KMtXAhjBcTyziYrLNueKNiOSWrAFK # u75xqRdbZ2De+JKRHh09/SDPc31BmkZ1zcRfNN0Sidb9pSB9fvzZnkXftnIv231f # gLrbqn427DZM9ituqBJR6L8FA6PRc6ZNN3SUHDSCD/AQ8rdHGO2n6Jl8P0zbr17C # 89XYcz1DTsEzOUyOArxCaC4Q6oRRRuLRvWoYWmEBc8pnol7XKHYC4jMYctenIPDC # +hIK12NvDMk2ZItboKaDIV1fMHSRlJTYuVD5C4lh8zYGNRiER9vcG9H9stQcxWv2 # XFJRXRLbJbqvUAV6bMURHXLvjflSxIUXk8A8FdsaN8cIFRg/eKtFtvUeh17aj54W # cmnGrnu3tz5q4i6tAgMBAAGjggHdMIIB2TASBgkrBgEEAYI3FQEEBQIDAQABMCMG # CSsGAQQBgjcVAgQWBBQqp1L+ZMSavoKRPEY1Kc8Q/y8E7jAdBgNVHQ4EFgQUn6cV # XQBeYl2D9OXSZacbUzUZ6XIwXAYDVR0gBFUwUzBRBgwrBgEEAYI3TIN9AQEwQTA/ # BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9Eb2Nz # L1JlcG9zaXRvcnkuaHRtMBMGA1UdJQQMMAoGCCsGAQUFBwMIMBkGCSsGAQQBgjcU # AgQMHgoAUwB1AGIAQwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8G # A1UdIwQYMBaAFNX2VsuP6KJcYmjRPZSQW9fOmhjEMFYGA1UdHwRPME0wS6BJoEeG # RWh0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jv # b0NlckF1dF8yMDEwLTA2LTIzLmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYBBQUH # MAKGPmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2Vy # QXV0XzIwMTAtMDYtMjMuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQCdVX38Kq3hLB9n # ATEkW+Geckv8qW/qXBS2Pk5HZHixBpOXPTEztTnXwnE2P9pkbHzQdTltuw8x5MKP # +2zRoZQYIu7pZmc6U03dmLq2HnjYNi6cqYJWAAOwBb6J6Gngugnue99qb74py27Y # P0h1AdkY3m2CDPVtI1TkeFN1JFe53Z/zjj3G82jfZfakVqr3lbYoVSfQJL1AoL8Z # thISEV09J+BAljis9/kpicO8F7BUhUKz/AyeixmJ5/ALaoHCgRlCGVJ1ijbCHcNh # cy4sa3tuPywJeBTpkbKpW99Jo3QMvOyRgNI95ko+ZjtPu4b6MhrZlvSP9pEB9s7G # dP32THJvEKt1MMU0sHrYUP4KWN1APMdUbZ1jdEgssU5HLcEUBHG/ZPkkvnNtyo4J # vbMBV0lUZNlz138eW0QBjloZkWsNn6Qo3GcZKCS6OEuabvshVGtqRRFHqfG3rsjo # iV5PndLQTHa1V1QJsWkBRH58oWFsc/4Ku+xBZj1p/cvBQUl+fpO+y/g75LcVv7TO # PqUxUYS8vwLBgqJ7Fx0ViY1w/ue10CgaiQuPNtq6TPmb/wrpNPgkNWcr4A245oyZ # 1uEi6vAnQj0llOZ0dFtq0Z4+7X6gMTN9vMvpe784cETRkPHIqzqKOghif9lwY1NN # je6CbaUFEMFxBmoQtB1VM1izoXBm8qGCAs4wggI3AgEBMIH4oYHQpIHNMIHKMQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNy # b3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVT # TjpENkJELUUzRTctMTY4NTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAg # U2VydmljZaIjCgEBMAcGBSsOAwIaAxUA4gBI/QlJu/lHbfDFyJCK8fJyRiiggYMw # gYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD # VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUF # AAIFAOhkWjMwIhgPMjAyMzA3MjEwOTEzMjNaGA8yMDIzMDcyMjA5MTMyM1owdzA9 # BgorBgEEAYRZCgQBMS8wLTAKAgUA6GRaMwIBADAKAgEAAgIjBgIB/zAHAgEAAgIR # 1DAKAgUA6GWrswIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAow # CAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GBAAxvyn86yily # CHqGrWt00c5f4IzxTvK+QGD+/a3QfhrLxvpGyhfCiRx9PUIPMsPSxsoiSZSEMVst # gsujFcfaySpGIbZJ9DRwhz+HeC8zfXF81sEqnWT+LHGooytcbdhRsNIfZ1oMD08g # p9xCucBJsNLvrcYfTK7xyzOC3AoeUiOfMYIEDTCCBAkCAQEwgZMwfDELMAkGA1UE # BhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAc # BgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0 # IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAHH+wCgSlvyJ9wAAQAAAccwDQYJYIZI # AWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG # 9w0BCQQxIgQgmuSFs/rl2mxCjTZE7v6UjA4amL6bB6ChUZ8B5s4ISDwwgfoGCyqG # SIb3DQEJEAIvMYHqMIHnMIHkMIG9BCBH5+Xb4lKyRQs55Vgtt4yCTsd0htESYCyP # C1zLowmSyTCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5n # dG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9y # YXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMz # AAABx/sAoEpb8ifcAAEAAAHHMCIEIB1EEG+BNsRJbNklxxQMxVHbolcBVPSkM4yc # ulkyH3neMA0GCSqGSIb3DQEBCwUABIICAEgvQaB4MA4n2a8psnwGQrbSECJtNEEe # umdhXFbVe8P3tPGIeuDrpeNQL3XHEjqAQlA6KPT6gbHGNsdjgMXDngt5XWXv8di3 # ziSvG8Mgk+kbtzOGp+dnccqgq3W2qbFv01OIFV/OVFgQi2tp6GVo062d0yZc66/z # mfXRbXFgHO1TX0u5+fnTC6T/zwiCJDch0ug2VTO1ruJ1x2TXiPcFlsDmMSsklnzX # 8cVN91ynGObIHoBt/jLAw0GNaV0CU+ZpaWm9p+2tp/B50EhArHyApjoLQektHLiz # wZ/c93WJN588/32O75RuEOoSzHenKoeQVYAXPmc3g6m7JVBFmiCzvKhmLLoe/lug # 97OXXCd5em8D1faq+tkjr60VpvB8a2uKZ64xn9CaNv9YnciAiWdaz7gFF8ajAKG/ # XvyzC4/laRgMfnU2o4aUidr0u9kf8RMfXsa/VRnVvyGrCkcnYOYmQi5y9xpAdfRR # OwW4kUYu20h1yOcYr8O85NgYdfSJGsYTJSRbUOVO75M1QaH9LrlRwvAeNYPZAk1Z # lavYOPyAJL2NPBoM/4zGcXMIIOevFOaQUfINAk1JLwlGIMQ441t8j10URqH59kJ0 # a+zedo6kVFtxZS1Wp826VQV3834CwB+vwTGujixFKNMvC0MLkqW9qCKsW/zVARTa # Dqg5diSBCOWa # SIG # End signature block |