GuestConfiguration.psm1
#Region './prefix.ps1' 0 $currentCulture = [System.Globalization.CultureInfo]::CurrentCulture if ($currentCulture.Name -eq 'en-US-POSIX') { throw "'$($currentCulture.Name)' culture is not supported, please change to 'en-US'" } #EndRegion './prefix.ps1' 6 #Region './Private/ConvertTo-OrderedHashtable.ps1' 0 function ConvertTo-OrderedHashtable { [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] $InputObject ) if ($null -eq $InputObject) { $output = $null } elseif ($InputObject -is [PSCustomObject]) { $output = [Ordered]@{} foreach ($property in $InputObject.PSObject.Properties) { $propertyValue = ConvertTo-OrderedHashtable -InputObject $property.Value if ($property.Value -is [System.Collections.IEnumerable] -and $property.Value -isnot [string]) { $output[$property.Name] = @( $propertyValue ) } else { $output[$property.Name] = $propertyValue } } } elseif ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) { $output = @() foreach ($object in $InputObject) { $output += ConvertTo-OrderedHashtable -InputObject $object } } else { $output = $InputObject } return $output } #EndRegion './Private/ConvertTo-OrderedHashtable.ps1' 47 #Region './Private/Edit-GuestConfigurationPackageMofChefInSpecContent.ps1' 0 function Edit-GuestConfigurationPackageMofChefInSpecContent { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [String] $PackageName, [Parameter(Mandatory = $true)] [String] $MofPath ) Write-Verbose -Message "Editing the mof at '$MofPath' to update native InSpec resource parameters" $mofInstances = [Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache]::ImportInstances($MofPath, 4) foreach ($mofInstance in $mofInstances) { $resourceClassName = $mofInstance.CimClass.CimClassName if ($resourceClassName -ieq 'MSFT_ChefInSpecResource') { $profilePath = "$PackageName/Modules/$($mofInstance.Name)/" $gitHubPath = $mofInstance.CimInstanceProperties.Item('GithubPath') if ($null -eq $gitHubPath) { $gitHubPath = [Microsoft.Management.Infrastructure.CimProperty]::Create('GithubPath', $profilePath, [Microsoft.Management.Infrastructure.CimFlags]::Property) $mofInstance.CimInstanceProperties.Add($gitHubPath) } else { $gitHubPath.Value = $profilePath } } } Write-MofContent -MofInstances $mofInstances -OutputPath $MofPath } #EndRegion './Private/Edit-GuestConfigurationPackageMofChefInSpecContent.ps1' 43 #Region './Private/Format-PolicyDefinitionJson.ps1' 0 function Format-PolicyDefinitionJson { [CmdletBinding()] [OutputType([String])] param ( [Parameter(Mandatory = $true)] [String] $Json ) $indent = 0 $jsonLines = $Json -Split '\n' $formattedLines = @() $previousLine = '' foreach ($line in $jsonLines) { $skipAddingLine = $false if ($line -match '^\s*\}\s*' -or $line -match '^\s*\]\s*') { # This line contains ] or }, decrement the indentation level $indent-- } $formattedLine = (' ' * $indent * 4) + $line.TrimStart().Replace(': ', ': ') if ($line -match '\s*".*"\s*:\s*\[' -or $line -match '\s*".*"\s*:\s*\{' -or $line -match '^\s*\{\s*' -or $line -match '^\s*\[\s*') { # This line contains [ or {, increment the indentation level $indent++ } if ($previousLine.Trim().EndsWith("{")) { if ($formattedLine.Trim() -in @("}", "},")) { $newLine = "$($previousLine.TrimEnd())$($formattedLine.Trim())" #Write-Verbose -Message "FOUND SHORTENED LINE: $newLine" $formattedLines[($formattedLines.Count - 1)] = $newLine $previousLine = $newLine $skipAddingLine = $true } } if ($previousLine.Trim().EndsWith("[")) { if ($formattedLine.Trim() -in @("]", "],")) { $newLine = "$($previousLine.TrimEnd())$($formattedLine.Trim())" #Write-Verbose -Message "FOUND SHORTENED LINE: $newLine" $formattedLines[($formattedLines.Count - 1)] = $newLine $previousLine = $newLine $skipAddingLine = $true } } if (-not $skipAddingLine -and -not [String]::IsNullOrWhiteSpace($formattedLine)) { $previousLine = $formattedLine $formattedLines += $formattedLine } } $formattedJson = $formattedLines -join "`n" return $formattedJson } #EndRegion './Private/Format-PolicyDefinitionJson.ps1' 68 #Region './Private/Get-GCWorkerExePath.ps1' 0 function Get-GCWorkerExePath { [CmdletBinding()] [OutputType([String])] param () $gcWorkerFolderPath = Get-GCWorkerRootPath $binFolderPath = Join-Path -Path $gcWorkerFolderPath -ChildPath 'GC' $os = Get-OSPlatform if ($os -ieq 'Windows') { $gcWorkerExeName = 'gc_worker.exe' } else { $gcWorkerExeName = 'gc_worker' } $gcWorkerExePath = Join-Path -Path $binFolderPath -ChildPath $gcWorkerExeName return $gcWorkerExePath } #EndRegion './Private/Get-GCWorkerExePath.ps1' 25 #Region './Private/Get-GCWorkerRootPath.ps1' 0 function Get-GCWorkerRootPath { [CmdletBinding()] [OutputType([String])] param () $gcWorkerRootPath = Join-Path -Path $PSScriptRoot -ChildPath 'gcworker' return $gcWorkerRootPath } #EndRegion './Private/Get-GCWorkerRootPath.ps1' 10 #Region './Private/Get-GuestConfigurationPolicySectionFromTemplate.ps1' 0 function Get-GuestConfigurationPolicySectionFromTemplate { [CmdletBinding()] [OutputType([System.Collections.Specialized.OrderedDictionary])] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $FileName ) $templateFolderPath = Join-Path -Path $PSScriptRoot -ChildPath 'templates' $filePath = Join-Path -Path $templateFolderPath -ChildPath $FileName $fileContent = Get-Content -Path $filePath -Raw $fileContentObject = $fileContent | ConvertFrom-Json $fileContentHashtable = ConvertTo-OrderedHashtable -InputObject $fileContentObject return $fileContentHashtable } #EndRegion './Private/Get-GuestConfigurationPolicySectionFromTemplate.ps1' 23 #Region './Private/Get-ModuleDependencies.ps1' 0 function Get-ModuleDependencies { [CmdletBinding()] [OutputType([Hashtable[]])] param ( [Parameter(Mandatory = $true)] [String] $ModuleName, [Parameter()] [String] $ModuleVersion, [Parameter()] [String] $ModuleSourcePath = $env:PSModulePath ) $moduleDependencies = @() if ($ModuleName -ieq 'PSDesiredStateConfiguration') { throw "Found a dependency on resources from the PSDesiredStateConfiguration module, but we cannot copy these resources into the Guest Configuration package. Please switch these resources to using the PSDscResources module instead." } $getModuleParameters = @{ ListAvailable = $true } if ([String]::IsNullOrWhiteSpace($ModuleVersion)) { Write-Verbose -Message "Searching for a module with the name '$ModuleName'..." $getModuleParameters['Name'] = $ModuleName } else { Write-Verbose -Message "Searching for a module with the name '$ModuleName' and version '$ModuleVersion'..." $getModuleParameters['FullyQualifiedName'] = @{ ModuleName = $ModuleName ModuleVersion = $ModuleVersion } } $originalPSModulePath = $env:PSModulePath try { $env:PSModulePath = $ModuleSourcePath $sourceModule = Get-Module @getModuleParameters } finally { $env:PSModulePath = $originalPSModulePath } if ($null -eq $sourceModule) { throw "Failed to find a module with the name '$ModuleName' and the version '$ModuleVersion'. Please check that the module is installed and available in your PSModulePath." } elseif ('Count' -in $sourceModule.PSObject.Properties.Name -and $sourceModule.Count -gt 1) { Write-Verbose -Message "Found $($sourceModule.Count) modules with the name '$ModuleName'..." $sourceModule = ($sourceModule | Sort-Object -Property 'Version' -Descending)[0] Write-Warning -Message "Found more than one module with the name '$ModuleName'. Using the version '$($sourceModule.Version)'." } $moduleDependency = @{ Name = $resourceDependency['ModuleName'] Version = $resourceDependency['ModuleVersion'] SourcePath = $sourceModule.ModuleBase } $moduleDependencies += $moduleDependency # Add any modules required by this module to the package if ('RequiredModules' -in $sourceModule.PSObject.Properties.Name -and $null -ne $sourceModule.RequiredModules -and $sourceModule.RequiredModules.Count -gt 0) { foreach ($requiredModule in $sourceModule.RequiredModules) { Write-Verbose -Message "The module '$ModuleName' requires the module '$($requiredModule.Name)'. Attempting to copy the required module..." $getModuleDependenciesParameters = @{ ModuleName = $requiredModule.Name ModuleVersion = $requiredModule.Version } $moduleDependencies += Get-ModuleDependencies @getModuleDependenciesParameters } } # Add any modules marked as external module dependencies by this module to the package if ('ExternalModuleDependencies' -in $sourceModule.PSObject.Properties.Name -and $null -ne $sourceModule.ExternalModuleDependencies -and $sourceModule.ExternalModuleDependencies.Count -gt 0) { foreach ($externalModuleDependency in $sourceModule.ExternalModuleDependencies) { Write-Verbose -Message "The module '$ModuleName' requires the module '$externalModuleDependency'. Attempting to copy the required module..." $getModuleDependenciesParameters = @{ ModuleName = $requiredModule.Name } $moduleDependencies += Get-ModuleDependencies @getModuleDependenciesParameters } } return $moduleDependencies } #EndRegion './Private/Get-ModuleDependencies.ps1' 110 #Region './Private/Get-MofResouceDependencies.ps1' 0 function Get-MofResouceDependencies { [CmdletBinding()] [OutputType([Hashtable[]])] param ( [Parameter(Mandatory = $true)] [System.IO.FileInfo] $MofFilePath ) $MofFilePath = Resolve-RelativePath -Path $MofFilePath $resourceDependencies = @() $reservedResourceNames = @('OMI_ConfigurationDocument') $mofInstances = [Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache]::ImportInstances($mofFilePath, 4) foreach ($mofInstance in $mofInstances) { if ($reservedResourceNames -inotcontains $mofInstance.CimClass.CimClassName -and $mofInstance.CimInstanceProperties.Name -icontains 'ModuleName') { $instanceName = "" if ($mofInstance.CimInstanceProperties.Name -icontains 'Name') { $instanceName = $mofInstance.CimInstanceProperties['Name'].Value } Write-Verbose -Message "Found resource dependency in mof with instance name '$instanceName' and resource name '$($mofInstance.CimClass.CimClassName)' from module '$($mofInstance.ModuleName)' with version '$($mofInstance.ModuleVersion)'." $resourceDependencies += @{ ResourceInstanceName = $instanceName ResourceName = $mofInstance.CimClass.CimClassName ModuleName = $mofInstance.ModuleName ModuleVersion = $mofInstance.ModuleVersion } } } Write-Verbose -Message "Found $($resourceDependencies.Count) resource dependencies in the mof." return $resourceDependencies } #EndRegion './Private/Get-MofResouceDependencies.ps1' 42 #Region './Private/Get-OSPlatform.ps1' 0 function Get-OSPlatform { [CmdletBinding()] [OutputType([String])] param () $platform = 'Windows' if ($PSVersionTable.PSEdition -eq 'Desktop') { $platform = 'Windows' } elseif ($PSVersionTable.PSEdition -eq 'Core') { if ($IsWindows) { $platform = 'Windows' } elseif ($IsLinux) { $platform = 'Linux' } elseif ($IsMacOS) { $platform = 'MacOS' } } $platform } #EndRegion './Private/Get-OSPlatform.ps1' 31 #Region './Private/Install-GCWorker.ps1' 0 function Install-GCWorker { [CmdletBinding()] param ( [Parameter()] [Switch] $Force ) $workerInstallPath = Get-GCWorkerRootPath $logsFolderPath = Join-Path -Path $workerInstallPath -ChildPath 'logs' if (Test-Path -Path $workerInstallPath -PathType 'Container') { if ($Force) { $null = Remove-Item -Path $workerInstallPath -Recurse -Force $null = New-Item -Path $workerInstallPath -ItemType 'Directory' } } else { $null = New-Item -Path $workerInstallPath -ItemType 'Directory' } # The worker has 'GC' hard-coded internally, so if you change this file name, it will not work $binFolderDestinationPath = Join-Path -Path $workerInstallPath -ChildPath 'GC' if (-not (Test-Path -Path $binFolderDestinationPath -PathType 'Container')) { $null = New-Item -Path $binFolderDestinationPath -ItemType 'Directory' $binFolderSourcePath = Join-Path -Path $PSScriptRoot -ChildPath 'bin' $os = Get-OSPlatform if ($os -ieq 'Windows') { $windowsPackageSourcePath = Join-Path -Path $binFolderSourcePath -ChildPath 'DSC_Windows.zip' $null = Expand-Archive -Path $windowsPackageSourcePath -DestinationPath $binFolderDestinationPath } else { # The Linux package contains an additional folder level $linuxPackageSourcePath = Join-Path -Path $binFolderSourcePath -ChildPath 'DSC_Linux.zip' $null = Expand-Archive -Path $linuxPackageSourcePath -DestinationPath $workerInstallPath if (-not (Test-Path -Path $binFolderDestinationPath -PathType 'Container')) { throw "Linux agent package structure has changed. Expected a 'GC' folder within the package but the folder '$binFolderDestinationPath' does not exist." } # Fix for “LTTng-UST: Error (-17) while registering tracepoint probe. Duplicate registration of tracepoint probes having the same name is not allowed.” $tracePointProviderLibPath = Join-Path -Path $binFolderDestinationPath -ChildPath 'libcoreclrtraceptprovider.so' if (Test-Path -Path $tracePointProviderLibPath) { $null = Remove-Item -Path $tracePointProviderLibPath -Force } $bashFilesInBinFolder = @(Get-ChildItem -Path $binFolderDestinationPath -Filter "*.sh" -Recurse) foreach ($bashFileInBinFolder in $bashFilesInBinFolder) { chmod '+x' $bashFileInBinFolder.FullName } # Give root user permission to execute gc_worker chmod 700 $binFolderDestinationPath $gcWorkerExePath = Join-Path -Path $binFolderDestinationPath -ChildPath 'gc_worker' chmod '+x' $gcWorkerExePath } $logPath = Join-Path -Path $logsFolderPath -ChildPath 'gc_worker.log' $telemetryPath = Join-Path -Path $logsFolderPath -ChildPath 'gc_worker_telemetry.txt' $configurationsFolderPath = Join-Path -Path $workerInstallPath -ChildPath 'Configurations' $modulePath = Join-Path -Path $binFolderDestinationPath -ChildPath 'Modules' # The directory paths in gc.config must have trailing slashes $basePath = $workerInstallPath + [System.IO.Path]::DirectorySeparatorChar $binPath = $binFolderDestinationPath + [System.IO.Path]::DirectorySeparatorChar $configurationsFolderPath = $configurationsFolderPath + [System.IO.Path]::DirectorySeparatorChar $modulePath = $modulePath + [System.IO.Path]::DirectorySeparatorChar # Save GC config settings file $gcConfig = @{ "DoNotSendReport" = $true "SaveLogsInJsonFormat" = $true "Paths" = @{ "BasePath" = $basePath "DotNetFrameworkPath" = $binPath "UserConfigurationsPath" = $configurationsFolderPath "ModulePath" = $modulePath "LogPath" = $logPath "TelemetryPath" = $telemetryPath } } $gcConfigContent = $gcConfig | ConvertTo-Json $gcConfigPath = Join-Path -Path $binFolderDestinationPath -ChildPath 'gc.config' $null = Set-Content -Path $gcConfigPath -Value $gcConfigContent -Encoding 'ascii' -Force } else { Write-Verbose -Message "Guest Configuration worker binaries already installed at '$binFolderDestinationPath'" } if (-not (Test-Path -Path $logsFolderPath -PathType 'Container')) { Write-Verbose -Message "Creating the logs folder at '$logsFolderPath'" $null = New-Item -Path $logsFolderPath -ItemType 'Directory' } } #EndRegion './Private/Install-GCWorker.ps1' 117 #Region './Private/Invoke-GCWorker.ps1' 0 function Invoke-GCWorker { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $Arguments ) # Remove the logs if needed $gcWorkerFolderPath = Get-GCWorkerRootPath $gcLogPath = Join-Path -Path $gcWorkerFolderPath -ChildPath 'logs' $standardOutputPath = Join-Path -Path $gcLogPath -ChildPath 'gcworker_stdout.txt' if (Test-Path -Path $gcLogPath) { $null = Remove-Item -Path $gcLogPath -Recurse -Force } # Linux requires that this path already exists when GC worker is run $null = New-Item -Path $gcLogPath -ItemType 'Directory' -Force # Execute the publish operation through GC worker $gcWorkerExePath = Get-GCWorkerExePath $gcEnvPath = Split-Path -Path $gcWorkerExePath -Parent $originalEnvPath = $env:Path $envPathPieces = $env:Path -split ';' if ($envPathPieces -notcontains $gcEnvPath) { $env:Path = "$originalEnvPath;$gcEnvPath" } try { Write-Verbose -Message "Invoking GC worker with the arguments '$Arguments'" $null = Start-Process -FilePath $gcWorkerExePath -ArgumentList $Arguments -Wait -NoNewWindow -RedirectStandardOutput $standardOutputPath } finally { $env:Path = $originalEnvPath } # Wait for streams to close Start-Sleep -Seconds 1 # Write output Write-GuestConfigurationLogsToConsole } #EndRegion './Private/Invoke-GCWorker.ps1' 53 #Region './Private/Invoke-GCWorkerRun.ps1' 0 function Invoke-GCWorkerRun { [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $ConfigurationName, [Parameter()] [Switch] $Apply ) # Remove any existing reports if needed $gcWorkerFolderPath = Get-GCWorkerRootPath $reportsFolderPath = Join-Path -Path $gcWorkerFolderPath -ChildPath 'reports' if (Test-Path -Path $reportsFolderPath) { $null = Remove-Item -Path $reportsFolderPath -Recurse -Force } $arguments = "-o run_consistency -a $ConfigurationName -r " if ($Apply) { $arguments += "-s inguest_apply_and_monitor " } $arguments += "-c Pending" Invoke-GCWorker -Arguments $arguments $compliantReportFileName = "{0}_Compliant.json" -f $ConfigurationName $compliantReportFilePath = Join-Path -Path $reportsFolderPath -ChildPath $compliantReportFileName $compliantReportFileExists = Test-Path -Path $compliantReportFilePath -PathType 'Leaf' $nonCompliantReportFileName = "{0}_NonCompliant.json" -f $ConfigurationName $nonCompliantReportFilePath = Join-Path -Path $reportsFolderPath -ChildPath $nonCompliantReportFileName $nonCompliantReportFileExists = Test-Path -Path $nonCompliantReportFilePath -PathType 'Leaf' if ($compliantReportFileExists -and $nonCompliantReportFileExists) { Write-Warning -Message "Both a compliant report and non-compliant report exist for the package $ConfigurationName" $compliantReportFile = Get-Item -Path $compliantReportFilePath $nonCompliantReportFile = Get-Item -Path $nonCompliantReportFilePath if ($compliantReportFile.LastWriteTime -gt $nonCompliantReportFile.LastWriteTime) { Write-Warning -Message "Using last compliant report since it has a later LastWriteTime" $reportFilePath = $compliantReportFilePath } elseif ($compliantReportFile.LastWriteTime -lt $nonCompliantReportFile.LastWriteTime) { Write-Warning -Message "Using last non-compliant report since it has a later LastWriteTime" $reportFilePath = $nonCompliantReportFilePath } else { throw "The reports have the same LastWriteTime. Please remove the reports under '$reportsFolderPath' and try again." } } elseif ($compliantReportFileExists) { $reportFilePath = $compliantReportFilePath } elseif ($nonCompliantReportFileExists) { $reportFilePath = $nonCompliantReportFilePath } else { $logPath = Join-Path -Path $gcWorkerFolderPath -ChildPath 'logs' throw "No report was generated. The package likely was not formed correctly or crashed. Please check the logs under the path '$logPath'." } $reportContent = Get-Content -Path $reportFilePath -Raw $report = $reportContent | ConvertFrom-Json return $report } #EndRegion './Private/Invoke-GCWorkerRun.ps1' 86 #Region './Private/Invoke-GuestConfigurationPackage.ps1' 0 function Invoke-GuestConfigurationPackage { [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter(Position = 0, Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [System.IO.FileInfo] $Path, [Parameter()] [Hashtable[]] $Parameter = @(), [Parameter()] [Switch] $Apply ) $os = Get-OSPlatform if ($os -ieq 'MacOS') { throw 'This cmdlet is not supported on MacOS.' } #-----VALIDATE PARAMETERS----- $requiredParameterProperties = @('ResourceType', 'ResourceId', 'ResourcePropertyName', 'ResourcePropertyValue') foreach ($parameterInfo in $Parameter) { foreach ($requiredParameterProperty in $requiredParameterProperties) { if (-not ($parameterInfo.Keys -contains $requiredParameterProperty)) { $requiredParameterPropertyString = $requiredParameterProperties -join ', ' throw "One of the specified parameters is missing the mandatory property '$requiredParameterProperty'. The mandatory properties for parameters are: $requiredParameterPropertyString" } if ($parameterInfo[$requiredParameterProperty] -isnot [string]) { $requiredParameterPropertyString = $requiredParameterProperties -join ', ' throw "The property '$requiredParameterProperty' of one of the specified parameters is not a string. All parameter property values must be strings." } } } #-----VALIDATE PACKAGE SETUP----- $Path = Resolve-RelativePath -Path $Path if (-not (Test-Path -Path $Path -PathType 'Leaf')) { throw "No zip file found at the path '$Path'. Please specify the file path to a compressed Guest Configuration package (.zip) with the Path parameter." } $sourceZipFile = Get-Item -Path $Path if ($sourceZipFile.Extension -ine '.zip') { throw "The file found at the path '$Path' is not a .zip file. It has extension '$($sourceZipFile.Extension)'. Please specify the file path to a compressed Guest Configuration package (.zip) with the Path parameter." } # Install the Guest Configuration worker if needed Install-GCWorker # Extract the package $gcWorkerPath = Get-GCWorkerRootPath $gcWorkerPackagesFolderPath = Join-Path -Path $gcWorkerPath -ChildPath 'packages' $packageInstallFolderName = $sourceZipFile.BaseName $packageInstallPath = Join-Path -Path $gcWorkerPackagesFolderPath -ChildPath $packageInstallFolderName if (Test-Path -Path $packageInstallPath) { $null = Remove-Item -Path $packageInstallPath -Recurse -Force } $null = Expand-Archive -Path $Path -DestinationPath $packageInstallPath -Force # Find and validate the mof file $mofFilePattern = '*.mof' $mofChildItems = @( Get-ChildItem -Path $packageInstallPath -Filter $mofFilePattern -File ) if ($mofChildItems.Count -eq 0) { throw "No .mof file found in the package. The Guest Configuration package must include a compiled DSC configuration (.mof) with the same name as the package. Please use the New-GuestConfigurationPackage cmdlet to generate a valid package." } elseif ($mofChildItems.Count -gt 1) { throw "Found more than one .mof file in the extracted Guest Configuration package. Please remove any extra .mof files from the root of the package. Please use the New-GuestConfigurationPackage cmdlet to generate a valid package." } $mofFile = $mofChildItems[0] $packageName = $mofFile.BaseName # Get package version $metaconfigFileName = "{0}.metaconfig.json" -f $packageName $metaconfigFilePath = Join-Path -Path $packageInstallPath -ChildPath $metaconfigFileName if (Test-Path -Path $metaconfigFilePath) { $metaconfig = Get-Content -Path $metaconfigFilePath -Raw | ConvertFrom-Json | ConvertTo-OrderedHashtable if ($metaconfig.Keys -contains 'Version') { $packageVersion = $metaconfig['Version'] Write-Verbose -Message "Package has the version $packageVersion" } if ($metaconfig.Keys -contains 'Type') { $packageType = $metaconfig['Type'] Write-Verbose -Message "Package has the type $packageType" if ($packageType -eq 'Audit' -and $Apply) { throw 'The specified package has been marked as Audit-only. You cannot apply/remediate an Audit-only package. Please change the type of the package to "AuditAndSet" with New-GuestConfigurationPackage if you would like to apply/remediate with this package.' } } else { Write-Warning -Message "Failed to determine the package type from the metaconfig file '$metaconfigFileName' in the package. Please use the latest version of the New-GuestConfigurationPackage cmdlet to construct your package." } } else { Write-Warning -Message "Failed to find the metaconfig file '$metaconfigFileName' in the package. Please use the latest version of the New-GuestConfigurationPackage cmdlet to construct your package." } # Rename the package install folder to match what the GC worker expects if needed if ($packageName -ne $packageInstallFolderName) { $newPackageInstallPath = Join-Path -Path $gcWorkerPackagesFolderPath -ChildPath $packageName if (Test-Path -Path $newPackageInstallPath) { $null = Remove-Item -Path $newPackageInstallPath -Recurse -Force } $null = Rename-Item -Path $packageInstallPath -NewName $newPackageInstallPath $packageInstallPath = $newPackageInstallPath } $mofFilePath = Join-Path -Path $packageInstallPath -ChildPath $mofFile.Name # Validate dependencies $resourceDependencies = @( Get-MofResouceDependencies -MofFilePath $mofFilePath ) if ($resourceDependencies.Count -le 0) { throw "Failed to determine resource dependencies from the .mof file in the package. The Guest Configuration package must include a compiled DSC configuration (.mof) with the same name as the package. Please use the New-GuestConfigurationPackage cmdlet to generate a valid package." } $usingInSpecResource = $false $moduleDependencies = @() $inSpecProfileNames = @() $modulesFolderPath = Join-Path -Path $packageInstallPath -ChildPath 'Modules' foreach ($resourceDependency in $resourceDependencies) { if ($resourceDependency['ResourceName'] -ieq 'MSFT_ChefInSpecResource') { $usingInSpecResource = $true $inSpecProfileNames += $resourceDependency['ResourceInstanceName'] continue } $getModuleDependenciesParameters = @{ ModuleName = $resourceDependency['ModuleName'] ModuleVersion = $resourceDependency['ModuleVersion'] ModuleSourcePath = $modulesFolderPath } $moduleDependencies += Get-ModuleDependencies @getModuleDependenciesParameters } if ($moduleDependencies.Count -gt 0) { Write-Verbose -Message "Found the module dependencies: $($moduleDependencies.Name)" } $duplicateModules = @( $moduleDependencies | Group-Object -Property 'Name' | Where-Object { $_.Count -gt 1 } ) foreach ($duplicateModule in $duplicateModules) { $uniqueVersions = @( $duplicateModule.Group.Version | Get-Unique ) if ($uniqueVersions.Count -gt 1) { $moduleName = $duplicateModule.Group[0].Name throw "Cannot include more than one version of a module in one package. Detected versions $uniqueVersions of the module '$moduleName' are needed for this package." } } if ($usingInSpecResource) { $metaConfigName = "$packageName.metaconfig.json" $metaConfigPath = Join-Path -Path $packageInstallPath -ChildPath $metaConfigName $metaConfigContent = Get-Content -Path $metaConfigPath -Raw $metaConfig = $metaConfigContent | ConvertFrom-Json $packageType = $metaConfig.Type if ($packageType -ieq 'AuditAndSet') { throw "The type of this package was specified as 'AuditAndSet', but native InSpec resource was detected in the provided .mof file. This resource does not currently support the set scenario and can only be used for 'Audit' packages." } Write-Verbose -Message "Expecting the InSpec profiles: $($inSpecProfileNames)" foreach ($expectedInSpecProfileName in $inSpecProfileNames) { $inSpecProfilePath = Join-Path -Path $modulesFolderPath -ChildPath $expectedInSpecProfileName $inSpecProfile = Get-Item -Path $inSpecProfilePath -ErrorAction 'SilentlyContinue' if ($null -eq $inSpecProfile) { throw "Expected to find an InSpec profile at the path '$inSpecProfilePath', but there is no item at this path." } elseif ($inSpecProfile -isnot [System.IO.DirectoryInfo]) { throw "Expected to find an InSpec profile at the path '$inSpecProfilePath', but the item at this path is not a directory." } else { $inSpecProfileYmlFileName = 'inspec.yml' $inSpecProfileYmlFilePath = Join-Path -Path $inSpecProfilePath -ChildPath $inSpecProfileYmlFileName if (-not (Test-Path -Path $inSpecProfileYmlFilePath -PathType 'Leaf')) { throw "Expected to find an InSpec profile at the path '$inSpecProfilePath', but there is no file named '$inSpecProfileYmlFileName' under this path." } } } } #-----RUN PACKAGE----- # Update package metaconfig to use debug mode and force module imports $metaconfigName = "$packageName.metaconfig.json" $metaconfigPath = Join-Path -Path $packageInstallPath -ChildPath $metaconfigName $propertiesToUpdate = @{ debugMode = 'ForceModuleImport' } if ($Apply) { $propertiesToUpdate['configurationMode'] = 'ApplyAndMonitor' } Set-GuestConfigurationPackageMetaconfigProperty -MetaconfigPath $metaconfigPath -Property $propertiesToUpdate # Update package configuration parameters if ($null -ne $Parameter -and $Parameter.Count -gt 0) { Set-GuestConfigurationPackageParameters -Path $mofFilePath -Parameter $Parameter } # Publish the package via GC worker Publish-GCWorkerAssignment -PackagePath $packageInstallPath # Set GC worker settings for the package Set-GCWorkerSettings -PackagePath $packageInstallPath # Invoke GC worker $result = Invoke-GCWorkerRun -ConfigurationName $packageName -Apply:$Apply return $result } #EndRegion './Private/Invoke-GuestConfigurationPackage.ps1' 270 #Region './Private/New-GuestConfigurationPolicyActionSection.ps1' 0 function New-GuestConfigurationPolicyActionSection { [CmdletBinding()] [OutputType([System.Collections.Specialized.OrderedDictionary])] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $ConfigurationName, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $ConfigurationVersion, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $ContentUri, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $ContentHash, [Parameter()] [ValidateSet('Audit', 'ApplyAndAutoCorrect', 'ApplyAndMonitor')] [String] $AssignmentType = 'Audit', [Parameter()] [Hashtable[]] $Parameter ) if ($AssignmentType -ieq 'Audit') { $actionSection = New-GuestConfigurationPolicyAuditActionSection -ConfigurationName $ConfigurationName -Parameter $Parameter } else { $setActionSectionParameters = @{ ConfigurationName = $ConfigurationName ConfigurationVersion = $ConfigurationVersion ContentUri = $ContentUri ContentHash = $ContentHash AssignmentType = $AssignmentType Parameter = $Parameter } $actionSection = New-GuestConfigurationPolicySetActionSection @setActionSectionParameters } if ($null -ne $Parameter -and $Parameter.Count -gt 0) { $complianceCondition = $actionSection.details.existenceCondition $parameterHashStringList = @() foreach ($currentParameter in $Parameter) { $parameterReference = New-GuestConfigurationPolicyParameterReferenceString -Parameter $currentParameter $parameterValue = "parameters('$($currentParameter['Name'])')" $currentParameterHashString = "'$parameterReference', '=', $parameterValue" $parameterHashStringList += $currentParameterHashString } $concantenatedParameterHashStrings = $parameterHashStringList -join ", ',', " $parameterHashString = "[base64(concat($concantenatedParameterHashStrings))]" $parameterCondition = [Ordered]@{ field = 'Microsoft.GuestConfiguration/guestConfigurationAssignments/parameterHash' equals = $parameterHashString } $actionSection.details.existenceCondition = [Ordered]@{ allOf = @( $complianceCondition, $parameterCondition ) } } return $actionSection } #EndRegion './Private/New-GuestConfigurationPolicyActionSection.ps1' 87 #Region './Private/New-GuestConfigurationPolicyAuditActionSection.ps1' 0 function New-GuestConfigurationPolicyAuditActionSection { [CmdletBinding()] [OutputType([System.Collections.Specialized.OrderedDictionary])] param ( [Parameter(Mandatory = $true)] [String] $ConfigurationName, [Parameter()] [Hashtable[]] $Parameter ) $templateFileName = "4-Action-Audit.json" $auditActionSection = Get-GuestConfigurationPolicySectionFromTemplate -FileName $templateFileName $assignmentName = New-GuestConfigurationPolicyGuestAssignmentNameReference -ConfigurationName $ConfigurationName $auditActionSection.details.name = $assignmentName return $auditActionSection } #EndRegion './Private/New-GuestConfigurationPolicyAuditActionSection.ps1' 25 #Region './Private/New-GuestConfigurationPolicyConditionsSection.ps1' 0 function New-GuestConfigurationPolicyConditionsSection { [CmdletBinding()] [OutputType([System.Collections.Specialized.OrderedDictionary])] param ( [Parameter(Mandatory = $true)] [ValidateSet('Windows', 'Linux')] [String] $Platform, [Parameter()] [System.Collections.Hashtable] $Tag ) $templateFileName = "3-Images-$Platform.json" $conditionsSection = Get-GuestConfigurationPolicySectionFromTemplate -FileName $templateFileName if ($null -ne $Tag -and $Tag.Count -gt 0) { $tagConditionList = @() foreach ($tagName in $Tag.Keys) { $tagConditionList += [Ordered]@{ field = "tags['$tagName']" equals = $($Tag[$tagName]) } } if ($tagConditionList.Count -eq 1) { $tagConditions = $tagConditionList[0] } elseif ($tagConditionList.Count -gt 1) { $tagConditions = [Ordered]@{ allOf = $tagConditionList } } $conditionsSection = [Ordered]@{ allOf = @( $conditionsSection, $tagConditions ) } } return $conditionsSection } #EndRegion './Private/New-GuestConfigurationPolicyConditionsSection.ps1' 53 #Region './Private/New-GuestConfigurationPolicyContent.ps1' 0 function New-GuestConfigurationPolicyContent { [CmdletBinding()] [OutputType([System.Collections.Specialized.OrderedDictionary])] param ( [Parameter(Mandatory = $true)] [String] $DisplayName, [Parameter(Mandatory = $true)] [String] $Description, [Parameter(Mandatory = $true)] [String] $PolicyVersion, [Parameter(Mandatory = $true)] [String] $ConfigurationName, [Parameter(Mandatory = $true)] [String] $ConfigurationVersion, [Parameter(Mandatory = $true)] [String] $ContentUri, [Parameter(Mandatory = $true)] [String] $ContentHash, [Parameter(Mandatory = $true)] [ValidateSet('Windows', 'Linux')] [String] $Platform, [Parameter(Mandatory = $true)] [ValidateSet('Audit', 'ApplyAndMonitor', 'ApplyAndAutoCorrect')] [String] $AssignmentType, [Parameter(Mandatory = $true)] [ValidateNotNull()] [String] $PolicyId, [Parameter()] [Hashtable[]] $Parameter, [Parameter()] [Hashtable] $Tag ) $metadataSectionParameters = @{ DisplayName = $DisplayName Description = $Description PolicyVersion = $PolicyVersion ConfigurationName = $ConfigurationName ConfigurationVersion = $ConfigurationVersion ContentUri = $ContentUri ContentHash = $ContentHash Parameter = $Parameter } $metadataSection = New-GuestConfigurationPolicyMetadataSection @metadataSectionParameters $parametersSection = New-GuestConfigurationPolicyParametersSection -Parameter $Parameter $conditionsSection = New-GuestConfigurationPolicyConditionsSection -Platform $Platform -Tag $Tag $actionSectionParameters = @{ ConfigurationName = $ConfigurationName ConfigurationVersion = $ConfigurationVersion ContentUri = $ContentUri ContentHash = $ContentHash AssignmentType = $AssignmentType Parameter = $Parameter } $actionSection = New-GuestConfigurationPolicyActionSection @actionSectionParameters $policyDefinitionContent = [Ordered]@{ properties = $metadataSection + $parametersSection + [Ordered]@{ policyRule = [Ordered]@{ if = $conditionsSection then = $actionSection } } name = $PolicyId } return $policyDefinitionContent } #EndRegion './Private/New-GuestConfigurationPolicyContent.ps1' 99 #Region './Private/New-GuestConfigurationPolicyGuestAssignmentNameReference.ps1' 0 function New-GuestConfigurationPolicyGuestAssignmentNameReference { [CmdletBinding()] [OutputType([String])] param ( [Parameter(Mandatory = $true)] [String] $ConfigurationName ) $guestAssignmentName = "[concat('{0}`$pid', uniqueString(policy().assignmentId, policy().definitionReferenceId))]" -f $ConfigurationName return $guestAssignmentName } #EndRegion './Private/New-GuestConfigurationPolicyGuestAssignmentNameReference.ps1' 15 #Region './Private/New-GuestConfigurationPolicyMetadataSection.ps1' 0 function New-GuestConfigurationPolicyMetadataSection { [CmdletBinding()] [OutputType([System.Collections.Specialized.OrderedDictionary])] param ( [Parameter(Mandatory = $true)] [String] $DisplayName, [Parameter(Mandatory = $true)] [String] $Description, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $PolicyVersion, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $ConfigurationName, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $ConfigurationVersion, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $ContentUri, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $ContentHash, [Parameter()] [Hashtable[]] $Parameter ) $templateFileName = '1-Metadata.json' $propertiesSection = Get-GuestConfigurationPolicySectionFromTemplate -FileName $templateFileName $propertiesSection.displayName = $DisplayName $propertiesSection.description = $Description $propertiesSection.metadata.version = $PolicyVersion $propertiesSection.metadata.guestConfiguration = [Ordered]@{ name = $ConfigurationName version = $ConfigurationVersion contentType = 'Custom' contentUri = $ContentUri contentHash = $ContentHash } if ($null -ne $Parameter -and $Parameter.Count -gt 0) { $propertiesSection.metadata.guestConfiguration.configurationParameter = [Ordered]@{} foreach ($currentParameter in $Parameter) { $parameterName = $currentParameter['Name'] $parameterReferenceString = New-GuestConfigurationPolicyParameterReferenceString -Parameter $currentParameter $propertiesSection.metadata.guestConfiguration.configurationParameter.$parameterName = $parameterReferenceString } } return $propertiesSection } #EndRegion './Private/New-GuestConfigurationPolicyMetadataSection.ps1' 76 #Region './Private/New-GuestConfigurationPolicyParameterReferenceString.ps1' 0 function New-GuestConfigurationPolicyParameterReferenceString { [CmdletBinding()] [OutputType([String])] param ( [Parameter(Mandatory = $true)] [Hashtable] $Parameter ) return "[$($Parameter['ResourceType'])]$($Parameter['ResourceId']);$($Parameter['ResourcePropertyName'])" } #EndRegion './Private/New-GuestConfigurationPolicyParameterReferenceString.ps1' 14 #Region './Private/New-GuestConfigurationPolicyParametersSection.ps1' 0 function New-GuestConfigurationPolicyParametersSection { [CmdletBinding()] [OutputType([System.Collections.Specialized.OrderedDictionary])] param ( [Parameter()] [Hashtable[]] $Parameter ) $templateFileName = '2-Parameters.json' $parametersSection = Get-GuestConfigurationPolicySectionFromTemplate -FileName $templateFileName $optionalFields = @('AllowedValues', 'DefaultValue') foreach ($currentParameter in $Parameter) { $parameterName = $currentParameter['Name'] $parametersSection.parameters.$parameterName = [Ordered]@{ type = 'string' metadata = [Ordered]@{ displayName = $currentParameter['DisplayName'] description = $currentParameter['Description'] } } foreach ($optionalField in $optionalFields) { if ($currentParameter.Keys -contains $optionalField) { $fieldName = $optionalField.Substring(0, 1).ToLower() + $optionalField.Substring(1) $parametersSection.parameters.$parameterName.$fieldName = $currentParameter[$optionalField] } } } return $parametersSection } #EndRegion './Private/New-GuestConfigurationPolicyParametersSection.ps1' 40 #Region './Private/New-GuestConfigurationPolicySetActionSection.ps1' 0 function New-GuestConfigurationPolicySetActionSection { [CmdletBinding()] [OutputType([System.Collections.Specialized.OrderedDictionary])] param ( [Parameter(Mandatory = $true)] [String] $ConfigurationName, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $ConfigurationVersion, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $ContentUri, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $ContentHash, [Parameter(Mandatory = $true)] [ValidateSet('ApplyAndMonitor', 'ApplyAndAutoCorrect')] [String] $AssignmentType, [Parameter()] [Hashtable[]] $Parameter ) $templateFileName = "4-Action-Set.json" $setActionSection = Get-GuestConfigurationPolicySectionFromTemplate -FileName $templateFileName $assignmentName = New-GuestConfigurationPolicyGuestAssignmentNameReference -ConfigurationName $ConfigurationName $setActionSection.details.name = $assignmentName $setActionSection.details.deployment.properties.parameters.assignmentName.value = $assignmentName foreach ($currentParameter in $Parameter) { $parameterName = $currentParameter['Name'] $setActionSection.details.deployment.properties.parameters.$parameterName = [Ordered]@{ value = "[parameters('$parameterName')]" } $setActionSection.details.deployment.properties.template.parameters.$parameterName = [Ordered]@{ type = "string" } } $guestConfigMetadataSection = [Ordered]@{ name = $ConfigurationName version = $ConfigurationVersion contentType = 'Custom' contentUri = $ContentUri contentHash = $ContentHash assignmentType = $AssignmentType } if ($null -ne $Parameter -and $Parameter.Count -gt 0) { $guestConfigMetadataSection.configurationParameter = @() foreach ($currentParameter in $Parameter) { $parameterName = $currentParameter['Name'] $parameterReferenceString = New-GuestConfigurationPolicyParameterReferenceString -Parameter $currentParameter $guestConfigMetadataSection.configurationParameter += [Ordered]@{ name = $parameterReferenceString value = "[parameters('$parameterName')]" } } } $setActionSection.details.deployment.properties.template.resources[0].properties.guestConfiguration = $guestConfigMetadataSection $setActionSection.details.deployment.properties.template.resources[1].properties.guestConfiguration = $guestConfigMetadataSection return $setActionSection } #EndRegion './Private/New-GuestConfigurationPolicySetActionSection.ps1' 86 #Region './Private/Publish-GCWorkerAssignment.ps1' 0 function Publish-GCWorkerAssignment { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $PackagePath ) if (-not (Test-Path -Path $PackagePath)) { throw "No Guest Configuration package found at the path '$PackagePath'" } $PackagePath = Resolve-RelativePath -Path $PackagePath $packageName = [System.IO.Path]::GetFileNameWithoutExtension($PackagePath) if ($PackagePath.EndsWith([System.IO.Path]::DirectorySeparatorChar)) { $PackagePath = $PackagePath.TrimEnd([System.IO.Path]::DirectorySeparatorChar) } $arguments = "-o publish_assignment -a $packageName -p `"$PackagePath`"" Invoke-GCWorker -Arguments $arguments } #EndRegion './Private/Publish-GCWorkerAssignment.ps1' 29 #Region './Private/Reset-GCWorkerTempDirectory.ps1' 0 function Reset-GCWorkerTempDirectory { [CmdletBinding()] [OutputType([String])] param () $gcWorkerRootPath = Get-GCWorkerRootPath $tempPath = Join-Path -Path $gcWorkerRootPath -ChildPath 'temp' if (Test-Path -Path $tempPath) { $null = Remove-Item -Path $tempPath -Recurse -Force } $null = New-Item -Path $tempPath -ItemType 'Directory' -Force return $tempPath } #EndRegion './Private/Reset-GCWorkerTempDirectory.ps1' 19 #Region './Private/Resolve-RelativePath.ps1' 0 function Resolve-RelativePath { [CmdletBinding()] [OutputType([String])] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $Path ) # This one doesn't work in PS 5.1 #$currentLocation = Get-Location #$fullPath = [System.IO.Path]::GetFullPath($Path, $currentLocation) # This doesn't work when the path doesn't exist yet #$fullPath = Convert-Path -Path $Path $fullPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) return $fullPath } #EndRegion './Private/Resolve-RelativePath.ps1' 23 #Region './Private/Set-GCWorkerSettings.ps1' 0 function Set-GCWorkerSettings { [CmdletBinding()] param ( [Parameter(Position=1, Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $PackagePath ) if (-not (Test-Path -Path $PackagePath)) { throw "No Guest Configuration package found at the path '$PackagePath'" } $PackagePath = Resolve-RelativePath -Path $PackagePath $packageName = [System.IO.Path]::GetFileNameWithoutExtension($PackagePath) if ($PackagePath.EndsWith([System.IO.Path]::DirectorySeparatorChar)) { $PackagePath = $PackagePath.TrimEnd([System.IO.Path]::DirectorySeparatorChar) } $arguments = "-o set_agent_settings -a $packageName -p `"$PackagePath`"" Invoke-GCWorker -Arguments $arguments } #EndRegion './Private/Set-GCWorkerSettings.ps1' 29 #Region './Private/Set-GuestConfigurationPackageMetaconfigProperty.ps1' 0 function Set-GuestConfigurationPackageMetaconfigProperty { [CmdletBinding()] [OutputType([Hashtable])] param ( [Parameter(Mandatory = $true)] [String] $MetaconfigPath, [Parameter(Mandatory = $true)] [Hashtable] $Property ) if (Test-Path -Path $MetaconfigPath) { $metaconfigContent = Get-Content -Path $MetaconfigPath -Raw $metaconfig = $metaconfigContent | ConvertFrom-Json | ConvertTo-OrderedHashtable } else { $metaconfig = [Ordered]@{} } foreach ($propertyName in $Property.Keys) { $metaconfig[$propertyName] = $Property[$propertyName] } $metaconfigJson = $metaconfig | ConvertTo-Json Write-Verbose -Message "Setting the content of the package metaconfig at the path '$MetaconfigPath'..." $null = Set-Content -Path $MetaconfigPath -Value $metaconfigJson -Encoding 'ascii' -Force } #EndRegion './Private/Set-GuestConfigurationPackageMetaconfigProperty.ps1' 36 #Region './Private/Set-GuestConfigurationPackageParameters.ps1' 0 function Set-GuestConfigurationPackageParameters { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [String] $Path, [Parameter()] [Hashtable[]] $Parameter ) if ($Parameter.Count -eq 0) { return } $mofInstances = [Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache]::ImportInstances($Path, 4) foreach ($parameterInfo in $Parameter) { if ($parameterInfo.Keys -notcontains 'ResourceType') { throw "Policy parameter is missing a mandatory property 'ResourceType'. Please make sure that configuration resource type is specified in configuration parameter." } if ($parameterInfo.Keys -notcontains 'ResourceId') { throw "Policy parameter is missing a mandatory property 'ResourceId'. Please make sure that configuration resource Id is specified in configuration parameter." } if ($parameterInfo.Keys -notcontains 'ResourcePropertyName') { throw "Policy parameter is missing a mandatory property 'ResourcePropertyName'. Please make sure that configuration resource property name is specified in configuration parameter." } if ($parameterInfo.Keys -notcontains 'ResourcePropertyValue') { throw "Policy parameter is missing a mandatory property 'ResourcePropertyValue'. Please make sure that configuration resource property value is specified in configuration parameter." } $resourceId = "[$($parameterInfo.ResourceType)]$($parameterInfo.ResourceId)" $matchingMofInstance = @( $mofInstances | Where-Object { ($_.CimInstanceProperties.Name -contains 'ResourceID') -and ($_.CimInstanceProperties['ResourceID'].Value -ieq $resourceId) -and ($_.CimInstanceProperties.Name -icontains $parameterInfo.ResourcePropertyName) }) if ($null -eq $matchingMofInstance -or $matchingMofInstance.Count -eq 0) { throw "Failed to find a matching parameter reference with ResourceType:'$($parameterInfo.ResourceType)', ResourceId:'$($parameterInfo.ResourceId)' and ResourcePropertyName:'$($parameterInfo.ResourcePropertyName)' in the configuration. Please ensure that this resource instance exists in the configuration." } if ($matchingMofInstance.Count -gt 1) { throw "Found more than one matching parameter reference with ResourceType:'$($parameterInfo.ResourceType)', ResourceId:'$($parameterInfo.ResourceId)' and ResourcePropertyName:'$($parameterInfo.ResourcePropertyName)'. Please ensure that only one resource instance with this information exists in the configuration." } $mofInstanceParameter = $matchingMofInstance[0].CimInstanceProperties.Item($parameterInfo.ResourcePropertyName) $mofInstanceParameter.Value = $parameterInfo.ResourcePropertyValue } Write-MofContent -MofInstances $mofInstances -OutputPath $Path } #EndRegion './Private/Set-GuestConfigurationPackageParameters.ps1' 68 #Region './Private/Write-GuestConfigurationLogsToConsole.ps1' 0 function Write-GuestConfigurationLogsToConsole { [CmdletBinding()] param () $gcWorkerFolderPath = Get-GCWorkerRootPath $gcLogFolderPath = Join-Path -Path $gcWorkerFolderPath -ChildPath 'logs' $gcLogPath = Join-Path -Path $gcLogFolderPath -ChildPath 'gc_agent.json' if (Test-Path -Path $gcLogPath) { $gcLogContent = Get-Content -Path $gcLogPath -Raw $gcLog = $gcLogContent | ConvertFrom-Json foreach ($logEvent in $gcLog) { if ($logEvent.type -ieq 'warning') { Write-Verbose -Message $logEvent.message } elseif ($logEvent.type -ieq 'error') { Write-Error -Message $logEvent.message } else { Write-Verbose -Message $logEvent.message } } } } #EndRegion './Private/Write-GuestConfigurationLogsToConsole.ps1' 32 #Region './Private/Write-MofContent.ps1' 0 function Write-MofContent { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] $MofInstances, [Parameter(Mandatory = $true)] [String] $OutputPath ) $content = '' $resourceCount = 0 foreach ($mofInstance in $MofInstances) { $resourceClassName = $mofInstance.CimClass.CimClassName $content += "instance of $resourceClassName" if ($resourceClassName -ne 'OMI_ConfigurationDocument') { $content += ' as $' + "$resourceClassName$resourceCount" } $content += "`n{`n" foreach ($cimProperty in $mofInstance.CimInstanceProperties) { $content += " $($cimProperty.Name)" if ($cimProperty.CimType -eq 'StringArray') { $content += " = {""$($cimProperty.Value -replace '[""\\]','\$&')""}; `n" } else { $content += " = ""$($cimProperty.Value -replace '[""\\]','\$&')""; `n" } } $content += "};`n" $resourceCount++ } $null = Set-Content -Path $OutputPath -Value $content -Force } #EndRegion './Private/Write-MofContent.ps1' 48 #Region './Public/Get-GuestConfigurationPackageComplianceStatus.ps1' 0 <# .SYNOPSIS Runs the given Guest Configuration package to retrieve the compliance status of the package on the current machine. .PARAMETER Path The path to the Guest Configuration package file (.zip) to run. .PARAMETER Parameter A list of hashtables describing the parameters to use when running the package. Basic Example: $Parameter = @( @{ ResourceType = 'Service' ResourceId = 'windowsService' ResourcePropertyName = 'Name' ResourcePropertyValue = 'winrm' }, @{ ResourceType = 'Service' ResourceId = 'windowsService' ResourcePropertyName = 'Ensure' ResourcePropertyValue = 'Present' } ) Technical Example: The Guest Configuration agent will replace parameter values in the compiled DSC configuration (.mof) file in the package before running it. If your compiled DSC configuration (.mof) file looked like this: instance of TestFile as $TestFile1ref { ModuleName = "TestFileModule"; ModuleVersion = "1.0.0.0"; ResourceID = "[TestFile]MyTestFile"; <--- This is both the resource type and ID Path = "test.txt"; <--- Here is the name of the parameter that I want to change the value of Content = "default"; Ensure = "Present"; SourceInfo = "TestFileSource"; ConfigurationName = "TestFileConfig"; }; Then your parameter value would look like this: $Parameter = @( @{ ResourceType = 'TestFile' ResourceId = 'MyTestFile' ResourcePropertyName = 'Path' ResourcePropertyValue = 'C:\myPath\newFile.txt' } ) .EXAMPLE Get-GuestConfigurationPackageComplianceStatus -Path ./custom_policy/WindowsTLS.zip .EXAMPLE $Parameter = @( @{ ResourceType = 'Service' ResourceId = 'windowsService' ResourcePropertyName = 'Name' ResourcePropertyValue = 'winrm' } ) Get-GuestConfigurationPackageComplianceStatus ` -Path ./custom_policy/AuditWindowsService.zip ` -Parameter $Parameter .OUTPUTS Returns a PSCustomObject with the report properties from running the package. Here is an example output: additionalProperties : {} assignmentName : TestFilePackage complianceStatus : False endTime : 5/9/2022 11:42:12 PM jobId : 18df23b4-cd22-4c26-b4b7-85b91873ec41 operationtype : Consistency resources : {@{complianceStatus=False; properties=; reasons=System.Object[]}} startTime : 5/9/2022 11:42:10 PM #> function Get-GuestConfigurationPackageComplianceStatus { [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter(Position = 0, Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [System.IO.FileInfo] $Path, [Parameter()] [Hashtable[]] $Parameter = @() ) $invokeParameters = @{ Path = $Path } if ($null -ne $Parameter) { $invokeParameters['Parameter'] = $Parameter } $result = Invoke-GuestConfigurationPackage @invokeParameters return $result } #EndRegion './Public/Get-GuestConfigurationPackageComplianceStatus.ps1' 115 #Region './Public/New-GuestConfigurationPackage.ps1' 0 <# .SYNOPSIS Creates a package to run code on machines through Azure Guest Configuration. .PARAMETER Name The name of the Guest Configuration package. .PARAMETER Configuration The path to the compiled DSC configuration file (.mof) to base the package on. .PARAMETER Version The semantic version of the Guest Configuration package. The default value is '1.0.0'. .PARAMETER Type Sets a tag in the metaconfig data of the package specifying whether or not this package is Audit-only or can support Set/Apply functionality. Audit indicates that the package will only monitor settings and cannot set the state of the machine. AuditAndSet indicates that the package can be used for both monitoring and setting the state of the machine. The default value is Audit. .PARAMETER FrequencyMinutes The frequency at which Guest Configuration should run this package in minutes. The default value is 15. 15 is also the mimimum value. Guest Configuration cannot run a package less-frequently than every 15 minutes. .PARAMETER Path The path to a folder to output the package under. By default the package will be created under the current working directory. .PARAMETER ChefInspecProfilePath The path to a folder containing Chef InSpec profiles to include with the package. The compiled DSC configuration (.mof) provided must include a reference to the native Chef InSpec resource with the reference name of the resources matching the name of the profile folder to use. If the compiled DSC configuration (.mof) provided includes a reference to the native Chef InSpec resource, then specifying a Chef InSpec profile to include with this parameter is required. .PARAMETER FilesToInclude The path(s) to any extra files or folders to include under the Modules path within the package. Please note, any files added here may not be accessible by custom modules. Files needed for custom modules need to be included within those modules. .PARAMETER Force If present, this function will overwrite any existing package files at the output path. .EXAMPLE New-GuestConfigurationPackage ` -Name 'WindowsTLS' ` -Configuration ./custom_policy/WindowsTLS/localhost.mof ` -Path ./git/repository/release/policy/WindowsTLS .OUTPUTS Returns a PSCustomObject with the name and path of the new Guest Configuration package. [PSCustomObject]@{ PSTypeName = 'GuestConfiguration.Package' Name = (Same as the Name parameter) Path = (Path to the newly created package zip file) } #> function New-GuestConfigurationPackage { [CmdletBinding()] [OutputType([PSCustomObject])] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] param ( [Parameter(Position = 0, Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [System.String] $Name, [Parameter(Position = 1, Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [System.IO.FileInfo] $Configuration, [Parameter(Position = 2, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [System.String] $Version = '1.0.0', [Parameter()] [ValidateSet('Audit', 'AuditAndSet')] [ValidateNotNullOrEmpty()] [String] $Type = 'Audit', [Parameter()] [int] $FrequencyMinutes = 15, [Parameter()] [ValidateNotNullOrEmpty()] [System.IO.DirectoryInfo] $Path = $(Get-Item -Path $(Get-Location)), [Parameter()] [System.IO.DirectoryInfo] $ChefInspecProfilePath, [Parameter()] [String[]] $FilesToInclude, [Parameter()] [Switch] $Force ) Write-Verbose -Message 'Starting New-GuestConfigurationPackage' $Configuration = Resolve-RelativePath -Path $Configuration $Path = Resolve-RelativePath -Path $Path if (-not [String]::IsNullOrEmpty($ChefInspecProfilePath)) { $ChefInspecProfilePath = Resolve-RelativePath -Path $ChefInspecProfilePath } #-----VALIDATION----- if ($FrequencyMinutes -lt 15) { throw "FrequencyMinutes must be 15 or greater. Guest Configuration cannot run packages more frequently than every 15 minutes." } # Validate mof if (-not (Test-Path -Path $Configuration -PathType 'Leaf')) { throw "No file found at the path '$Configuration'. Please specify the file path to a compiled DSC configuration (.mof) with the Configuration parameter." } $sourceMofFile = Get-Item -Path $Configuration if ($sourceMofFile.Extension -ine '.mof') { throw "The file found at the path '$Configuration' is not a .mof file. It has extension '$($sourceMofFile.Extension)'. Please specify the file path to a compiled DSC configuration (.mof) with the Configuration parameter." } # Validate dependencies $resourceDependencies = @( Get-MofResouceDependencies -MofFilePath $Configuration ) if ($resourceDependencies.Count -le 0) { throw "Failed to determine resource dependencies from the mof at the path '$Configuration'. Please specify the file path to a compiled DSC configuration (.mof) with the Configuration parameter." } $usingInSpecResource = $false $moduleDependencies = @() $inSpecProfileNames = @() foreach ($resourceDependency in $resourceDependencies) { if ($resourceDependency['ResourceName'] -ieq 'MSFT_ChefInSpecResource') { $usingInSpecResource = $true $inSpecProfileNames += $resourceDependency['ResourceInstanceName'] continue } $getModuleDependenciesParameters = @{ ModuleName = $resourceDependency['ModuleName'] ModuleVersion = $resourceDependency['ModuleVersion'] } $moduleDependencies += Get-ModuleDependencies @getModuleDependenciesParameters } if ($moduleDependencies.Count -gt 0) { Write-Verbose -Message "Found the module dependencies: $($moduleDependencies.Name)" } $duplicateModules = @( $moduleDependencies | Group-Object -Property 'Name' | Where-Object { $_.Count -gt 1 } ) foreach ($duplicateModule in $duplicateModules) { $uniqueVersions = @( $duplicateModule.Group.Version | Get-Unique ) if ($uniqueVersions.Count -gt 1) { $moduleName = $duplicateModule.Group[0].Name throw "Cannot include more than one version of a module in one package. Detected versions $uniqueVersions of the module '$moduleName' are needed for this package." } } $inSpecProfileSourcePaths = @() if ($usingInSpecResource) { Write-Verbose -Message "Expecting the InSpec profiles: $($inSpecProfileNames)" if ($Type -ieq 'AuditAndSet') { throw "The type of this package was specified as 'AuditAndSet', but native InSpec resource was detected in the provided .mof file. This resource does not currently support the set scenario and can only be used for 'Audit' packages." } if ([String]::IsNullOrEmpty($ChefInspecProfilePath)) { throw "The native InSpec resource was detected in the provided .mof file, but no InSpec profiles folder path was provided. Please provide the path to an InSpec profiles folder via the ChefInspecProfilePath parameter." } else { $inSpecProfileFolder = Get-Item -Path $ChefInspecProfilePath -ErrorAction 'SilentlyContinue' if ($null -eq $inSpecProfileFolder) { throw "The native InSpec resource was detected in the provided .mof file, but the specified path to the InSpec profiles folder does not exist. Please provide the path to an InSpec profiles folder via the ChefInspecProfilePath parameter." } elseif ($inSpecProfileFolder -isnot [System.IO.DirectoryInfo]) { throw "The native InSpec resource was detected in the provided .mof file, but the specified path to the InSpec profiles folder is not a directory. Please provide the path to an InSpec profiles folder via the ChefInspecProfilePath parameter." } else { foreach ($expectedInSpecProfileName in $inSpecProfileNames) { $inSpecProfilePath = Join-Path -Path $ChefInspecProfilePath -ChildPath $expectedInSpecProfileName $inSpecProfile = Get-Item -Path $inSpecProfilePath -ErrorAction 'SilentlyContinue' if ($null -eq $inSpecProfile) { throw "Expected to find an InSpec profile at the path '$inSpecProfilePath', but there is no item at this path." } elseif ($inSpecProfile -isnot [System.IO.DirectoryInfo]) { throw "Expected to find an InSpec profile at the path '$inSpecProfilePath', but the item at this path is not a directory." } else { $inSpecProfileYmlFileName = 'inspec.yml' $inSpecProfileYmlFilePath = Join-Path -Path $inSpecProfilePath -ChildPath $inSpecProfileYmlFileName if (Test-Path -Path $inSpecProfileYmlFilePath -PathType 'Leaf') { $inSpecProfileSourcePaths += $inSpecProfilePath } else { throw "Expected to find an InSpec profile at the path '$inSpecProfilePath', but there file named '$inSpecProfileYmlFileName' under this path." } } } } } } elseif (-not [String]::IsNullOrEmpty($ChefInspecProfilePath)) { throw "A Chef InSpec profile path was provided, but the native InSpec resource was not detected in the provided .mof file. Please provide a compiled DSC configuration (.mof) that references the native InSpec resource or remove the reference to the ChefInspecProfilePath parameter." } # Check extra files if needed foreach ($file in $FilesToInclude) { $filePath = Resolve-RelativePath -Path $file if (-not (Test-Path -Path $filePath)) { throw "The item to include from the path '$filePath' does not exist. Please update or remove the FilesToInclude parameter." } } # Check set-up folder $packageRootPath = Join-Path -Path $Path -ChildPath $Name if (Test-Path -Path $packageRootPath) { if (-not $Force) { throw "A folder already exists at the package folder path '$packageRootPath'. Please remove it or use the Force parameter. With -Force the cmdlet will remove this folder for you." } } # Check destination $packageDestinationPath = "$packageRootPath.zip" if (Test-Path -Path $packageDestinationPath) { if (-not $Force) { throw "A file already exists at the package destination path '$packageDestinationPath'. Please remove it or use the Force parameter. With -Force the cmdlet will remove this file for you." } } #-----PACKAGE CREATION----- # Clear the root package folder if (Test-Path -Path $packageRootPath) { if ($Configuration.FullName.Contains($packageRootPath)) { Write-Warning -Message "You have elected to forcibly remove the existing package folder path '$packageRootPath', but the specificed source path for the configuration document is under this path at '$Configuration'. The configuration document at this source path will be changed to match package requirements." $gcWorkerTempPath = Reset-GCWorkerTempDirectory $copiedMof = Copy-Item -Path $Configuration -Destination $gcWorkerTempPath -Force $Configuration = $copiedMof.FullName } Write-Verbose -Message "Removing an existing item at the path '$packageRootPath'..." $null = Remove-Item -Path $packageRootPath -Recurse -Force } Write-Verbose -Message "Creating the package root folder at the path '$packageRootPath'..." $null = New-Item -Path $packageRootPath -ItemType 'Directory' -Force # Clear the package destination if (Test-Path -Path $packageDestinationPath) { Write-Verbose -Message "Removing an existing item at the path '$packageDestinationPath'..." $null = Remove-Item -Path $packageDestinationPath -Recurse -Force } # Create the package structure $modulesFolderPath = Join-Path -Path $packageRootPath -ChildPath 'Modules' Write-Verbose -Message "Creating the package Modules folder at the path '$modulesFolderPath'..." $null = New-Item -Path $modulesFolderPath -ItemType 'Directory' # Create the metaconfig file $metaconfigFileName = "$Name.metaconfig.json" $metaconfigFilePath = Join-Path -Path $packageRootPath -ChildPath $metaconfigFileName $metaconfig = @{ Type = $Type Version = $Version } if ($FrequencyMinutes -gt 15) { $metaconfig['configurationModeFrequencyMins'] = $FrequencyMinutes } $metaconfigJson = $metaconfig | ConvertTo-Json Write-Verbose -Message "Setting the content of the package metaconfig at the path '$metaconfigFilePath'..." $null = Set-Content -Path $metaconfigFilePath -Value $metaconfigJson -Encoding 'ascii' # Copy the mof into the package $mofFileName = "$Name.mof" $mofFilePath = Join-Path -Path $packageRootPath -ChildPath $mofFileName Write-Verbose -Message "Copying the compiled DSC configuration (.mof) from the path '$Configuration' to the package path '$mofFilePath'..." $null = Copy-Item -Path $Configuration -Destination $mofFilePath # Edit the native Chef InSpec resource parameters in the mof if needed if ($usingInSpecResource) { Edit-GuestConfigurationPackageMofChefInSpecContent -PackageName $Name -MofPath $mofFilePath } # Copy resource dependencies foreach ($moduleDependency in $moduleDependencies) { $moduleDestinationPath = Join-Path -Path $modulesFolderPath -ChildPath $moduleDependency['Name'] Write-Verbose -Message "Copying module from '$($moduleDependency['SourcePath'])' to '$moduleDestinationPath'" $null = Copy-Item -Path $moduleDependency['SourcePath'] -Destination $moduleDestinationPath -Container -Recurse -Force } # Copy native Chef InSpec resource if needed if ($usingInSpecResource) { $nativeResourcesFolder = Join-Path -Path $modulesFolderPath -ChildPath 'DscNativeResources' Write-Verbose -Message "Creating the package native resources folder at the path '$nativeResourcesFolder'..." $null = New-Item -Path $nativeResourcesFolder -ItemType 'Directory' $inSpecResourceFolder = Join-Path -Path $nativeResourcesFolder -ChildPath 'MSFT_ChefInSpecResource' Write-Verbose -Message "Creating the native Chef InSpec resource folder at the path '$inSpecResourceFolder'..." $null = New-Item -Path $inSpecResourceFolder -ItemType 'Directory' $dscResourcesFolderPath = Join-Path -Path $PSScriptRoot -ChildPath 'DscResources' $inSpecResourceSourcePath = Join-Path -Path $dscResourcesFolderPath -ChildPath 'MSFT_ChefInSpecResource' $installInSpecScriptSourcePath = Join-Path -Path $inSpecResourceSourcePath -ChildPath 'install_inspec.sh' Write-Verbose -Message "Copying the Chef Inspec install script from the path '$installInSpecScriptSourcePath' to the package path '$modulesFolderPath'..." $null = Copy-Item -Path $installInSpecScriptSourcePath -Destination $modulesFolderPath $inSpecResourceLibrarySourcePath = Join-Path -Path $inSpecResourceSourcePath -ChildPath 'libMSFT_ChefInSpecResource.so' Write-Verbose -Message "Copying the native Chef Inspec resource library from the path '$inSpecResourceLibrarySourcePath' to the package path '$inSpecResourceFolder'..." $null = Copy-Item -Path $inSpecResourceLibrarySourcePath -Destination $inSpecResourceFolder $inSpecResourceSchemaMofSourcePath = Join-Path -Path $inSpecResourceSourcePath -ChildPath 'MSFT_ChefInSpecResource.schema.mof' Write-Verbose -Message "Copying the native Chef Inspec resource schema from the path '$inSpecResourceSchemaMofSourcePath' to the package path '$inSpecResourceFolder'..." $null = Copy-Item -Path $inSpecResourceSchemaMofSourcePath -Destination $inSpecResourceFolder foreach ($inSpecProfileSourcePath in $inSpecProfileSourcePaths) { Write-Verbose -Message "Copying the Chef Inspec profile from the path '$inSpecProfileSourcePath' to the package path '$modulesFolderPath'..." $null = Copy-Item -Path $inSpecProfileSourcePath -Destination $modulesFolderPath -Container -Recurse } } # Copy extra files foreach ($file in $FilesToInclude) { $filePath = Resolve-RelativePath -Path $file if (Test-Path -Path $filePath -PathType 'Leaf') { Write-Verbose -Message "Copying the custom file to include from the path '$filePath' to the package module path '$modulesFolderPath'..." $null = Copy-Item -Path $filePath -Destination $modulesFolderPath } else { Write-Verbose -Message "Copying the custom folder to include from the path '$filePath' to the package module path '$modulesFolderPath'..." $null = Copy-Item -Path $filePath -Destination $modulesFolderPath -Container -Recurse } } # Zip the package $compressArchiveSourcePath = Join-Path -Path $packageRootPath -ChildPath '*' Write-Verbose -Message "Compressing the generated package from the path '$compressArchiveSourcePath' to the package path '$packageDestinationPath'..." $null = Compress-Archive -Path $compressArchiveSourcePath -DestinationPath $packageDestinationPath -CompressionLevel 'Fastest' return [PSCustomObject]@{ PSTypeName = 'GuestConfiguration.Package' Name = $Name Path = $packageDestinationPath } } #EndRegion './Public/New-GuestConfigurationPackage.ps1' 427 #Region './Public/New-GuestConfigurationPolicy.ps1' 0 <# .SYNOPSIS Creates a policy definition to monitor and remediate settings on machines through Azure Guest Configuration and Azure Policy. .PARAMETER DisplayName The display name of the policy to create. The display name has a maximum length of 128 characters. .PARAMETER Description The description of the policy to create. The display name has a maximum length of 512 characters. .PARAMETER PolicyId The unique GUID of the policy definition. If you are trying to update an existing policy definition, then this ID must match the 'name' field in the existing definition. You can run New-Guid to generate a new GUID. .PARAMETER PolicyVersion The version of the policy definition. If you are updating an existing policy definition, then this version should be greater than the value in the 'metadata.version' field in the existing definition. Note: This is NOT the version of the Guest Configuration package. You can validate the Guest Configuration package version via the ContentVersion parameter. .PARAMETER ContentUri The public HTTP or HTTPS URI of the Guest Configuration package (.zip) to run via the created policy. Example: https://github.com/azure/auditservice/release/AuditService.zip .PARAMETER ContentVersion If specified, the version of the Guest Configuration package (.zip) downloaded via the content URI must match this value. This is for validation only. Note: This is NOT the version of the policy definition. You can define the policy definition version via the PolicyVersion parameter. .PARAMETER Path The path to the folder under which to create the new policy definition file. The default value is the 'definitions' folder under your GuestConfiguration module path. .PARAMETER Platform The target platform (Windows or Linux) for the policy. The default value is Windows. .PARAMETER Parameter The parameters to expose on the policy. All parameters passed to the policy must be single string values. Example: $policyParameters = @( @{ Name = 'ServiceName' # Required DisplayName = 'Windows Service Name' # Required Description = 'Name of the windows service to be audited.' # Required ResourceType = 'Service' # Required ResourceId = 'windowsService' # Required ResourcePropertyName = 'Name' # Required DefaultValue = 'winrm' # Optional AllowedValues = @('wscsvc', 'WSearch', 'wcncsvc', 'winrm') # Optional }, @{ Name = 'ServiceState' # Required DisplayName = 'Windows Service State' # Required Description = 'State of the windows service to be audited.' # Required ResourceType = 'Service' # Required ResourceId = 'windowsService' # Required ResourcePropertyName = 'State' # Required DefaultValue = 'Running' # Optional AllowedValues = @('Running', 'Disabled') # Optional } ) .PARAMETER Mode Defines the mode under which this policy should run the package on the machine. Allowed modes: Audit: Monitors the machine only. Will not make modifications to the machine. ApplyAndMonitor: Modifies the machine once if it does not match the expected state. Then monitors the machine only until another remediation task is triggered via Azure Policy. Will make modifications to the machine. ApplyAndAutoCorrect: Modifies the machine any time it does not match the expected state. You will need trigger a remediation task via Azure Policy to start modifications the first time. Will make modifications to the machine. The default value is Audit. If the package has been created as Audit-only, you cannot create an Apply policy with that package. The package will need to be re-created in AuditAndSet mode. .PARAMETER Tag A hashtable of the tags that should be on machines to apply this policy on. If this is specified, the created policy will only be applied to machines with all the specified tags. .EXAMPLE New-GuestConfigurationPolicy ` -ContentUri https://github.com/azure/auditservice/release/AuditService.zip ` -DisplayName 'Monitor Windows Service Policy.' ` -Description 'Policy to monitor service on Windows machine.' ` -PolicyVersion 1.1.0 ` -Path ./git/custom_policy ` -Tag @{ Owner = 'WebTeam' } .EXAMPLE $PolicyParameterInfo = @( @{ Name = 'ServiceName' # Policy parameter name (mandatory) DisplayName = 'windows service name.' # Policy parameter display name (mandatory) Description = "Name of the windows service to be audited." # Policy parameter description (mandatory) ResourceType = "Service" # configuration resource type (mandatory) ResourceId = 'windowsService' # configuration resource property name (mandatory) ResourcePropertyName = "Name" # configuration resource property name (mandatory) DefaultValue = 'winrm' # Policy parameter default value (optional) AllowedValues = @('wscsvc','WSearch','wcncsvc','winrm') # Policy parameter allowed values (optional) } ) New-GuestConfigurationPolicy -ContentUri 'https://github.com/azure/auditservice/release/AuditService.zip' ` -DisplayName 'Monitor Windows Service Policy.' ` -Description 'Policy to monitor service on Windows machine.' ` -PolicyId $myPolicyGuid ` -PolicyVersion 2.4.0 ` -Path ./policyDefinitions ` -Parameter $PolicyParameterInfo .OUTPUTS Returns the name and path of the Guest Configuration policy definition. This output can then be piped into New-AzPolicyDefinition. @{ PSTypeName = 'GuestConfiguration.Policy' Name = $policyName Path = $Path } #> function New-GuestConfigurationPolicy { [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $DisplayName, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Description, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.Guid] $PolicyId, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.Version] $PolicyVersion, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [System.Uri] $ContentUri, [Parameter()] [System.Version] $ContentVersion, [Parameter()] [ValidateNotNullOrEmpty()] [System.String] $Path, [Parameter()] [ValidateSet('Windows', 'Linux')] [System.String] $Platform = 'Windows', [Parameter()] [System.Collections.Hashtable[]] $Parameter, [Parameter()] [ValidateSet('Audit', 'ApplyAndAutoCorrect', 'ApplyAndMonitor')] [System.String] $Mode = 'Audit', [Parameter()] [System.Collections.Hashtable] $Tag ) # Validate parameters if ($DisplayName.Length -gt 128) { throw "The specified display name is more than the limit of 128 characters. Please specify a shorter display name." } if ($Description.Length -gt 512) { throw "The specified description is more than the limit of 512 characters. Please specify a shorter description." } if ($null -eq $ContentUri.AbsoluteURI) { throw "The specified package URI is not an absolute URI. Please specify a valid HTTP or HTTPS URI with the ContentUri parameter." } if ($ContentUri.Scheme -notmatch '[http|https]') { throw "The specified package URI does not follow the HTTP or HTTPS scheme. Please specify a valid HTTP or HTTPS URI with the ContentUri parameter." } $requiredParameterProperties = @('Name', 'DisplayName', 'Description', 'ResourceType', 'ResourceId', 'ResourcePropertyName') foreach ($parameterInfo in $Parameter) { foreach ($requiredParameterProperty in $requiredParameterProperties) { if (-not ($parameterInfo.Keys -contains $requiredParameterProperty)) { $requiredParameterPropertyString = $requiredParameterProperties -join ', ' throw "One of the specified policy parameters is missing the mandatory property '$requiredParameterProperty'. The mandatory properties for parameters are: $requiredParameterPropertyString" } if ($parameterInfo[$requiredParameterProperty] -isnot [string]) { $requiredParameterPropertyString = $requiredParameterProperties -join ', ' throw "The property '$requiredParameterProperty' of one of the specified parameters is not a string. All parameter property values must be strings." } } } # Download package $tempPath = Reset-GCWorkerTempDirectory $packageFileDownloadName = 'temp.zip' $packageFileDownloadPath = Join-Path -Path $tempPath -ChildPath $packageFileDownloadName if (Test-Path -Path $packageFileDownloadPath) { $null = Remove-Item -Path $packageFileDownloadPath -Force } $null = Invoke-WebRequest -Uri $ContentUri -OutFile $packageFileDownloadPath if ($null -eq (Get-Command -Name 'Get-FileHash' -ErrorAction 'SilentlyContinue')) { $null = Import-Module -Name 'Microsoft.PowerShell.Utility' } $contentHash = (Get-FileHash -Path $packageFileDownloadPath -Algorithm 'SHA256').Hash # Extract package $packagePath = Join-Path -Path $tempPath -ChildPath 'extracted' $null = Expand-Archive -Path $packageFileDownloadPath -DestinationPath $packagePath -Force # Get configuration name $mofFilePattern = '*.mof' $mofChildItems = @( Get-ChildItem -Path $packagePath -Filter $mofFilePattern -File ) if ($mofChildItems.Count -eq 0) { throw "No .mof file found in the extracted Guest Configuration package at '$packagePath'. The Guest Configuration package must include a compiled DSC configuration (.mof) with the same name as the package. Please use the New-GuestConfigurationPackage cmdlet to generate a valid package." } elseif ($mofChildItems.Count -gt 1) { throw "Found more than one .mof file in the extracted Guest Configuration package at '$packagePath'. Please remove any extra .mof files from the root of the package. Please use the New-GuestConfigurationPackage cmdlet to generate a valid package." } $mofFile = $mofChildItems[0] $packageName = $mofFile.BaseName # Get package version $packageVersion = '1.0.0' $metaconfigFileName = "{0}.metaconfig.json" -f $packageName $metaconfigFilePath =Join-Path -Path $packagePath -ChildPath $metaconfigFileName if (Test-Path -Path $metaconfigFilePath) { $metaconfig = Get-Content -Path $metaconfigFilePath -Raw | ConvertFrom-Json | ConvertTo-OrderedHashtable if ($metaconfig.Keys -contains 'Version') { $packageVersion = $metaconfig['Version'] Write-Verbose -Message "Downloaded package has the version $packageVersion" if ($null -ne $ContentVersion -and $ContentVersion -ne $packageVersion) { throw "Downloaded package version ($packageVersion) does not match specfied content version ($ContentVersion)." } } else { if ($null -eq $ContentVersion) { Write-Warning -Message "Failed to determine the package version from the metaconfig file '$metaconfigFileName' in the downloaded package. Please use the latest version of the New-GuestConfigurationPackage cmdlet to construct your package." } else { throw "Failed to determine the package version from the metaconfig file '$metaconfigFileName' in the downloaded package. Package version does not match specfied content version ($ContentVersion). Please use the latest version of the New-GuestConfigurationPackage cmdlet to construct your package." } } if ($metaconfig.Keys -contains 'Type') { $packageType = $metaconfig['Type'] Write-Verbose -Message "Downloaded package has the type $packageType" if ($packageType -eq 'Audit' -and $Mode -ne 'Audit') { throw 'The specified package has been marked as Audit-only. You cannot create an Apply policy with an Audit-only package. Please change the mode of the policy or the type of the package.' } } else { Write-Warning -Message "Failed to determine the package type from the metaconfig file '$metaconfigFileName' in the downloaded package. Please use the latest version of the New-GuestConfigurationPackage cmdlet to construct your package." } } else { Write-Warning -Message "Failed to find the metaconfig file '$metaconfigFileName' in the downloaded package. Please use the latest version of the New-GuestConfigurationPackage cmdlet to construct your package." } # Determine paths if ([String]::IsNullOrEmpty($Path)) { $Path = Get-Location } $Path = Resolve-RelativePath -Path $Path if (-not (Test-Path -Path $Path)) { $null = New-Item -Path $Path -ItemType 'Directory' -Force } # Determine if policy is AINE or DINE if ($Mode -eq 'Audit') { $fileName = '{0}_AuditIfNotExists.json' -f $packageName } else { $fileName = '{0}_DeployIfNotExists.json' -f $packageName } $filePath = Join-Path -Path $Path -ChildPath $fileName if (Test-Path -Path $filePath) { $null = Remove-Item -Path $filePath -Force } # Generate definition $policyDefinitionContentParameters = @{ DisplayName = $DisplayName Description = $Description PolicyVersion = $PolicyVersion ConfigurationName = $packageName ConfigurationVersion = $packageVersion ContentUri = $ContentUri ContentHash = $contentHash Platform = $Platform AssignmentType = $Mode PolicyId = $PolicyId Parameter = $Parameter Tag = $Tag } $policyDefinitionContent = New-GuestConfigurationPolicyContent @policyDefinitionContentParameters # Convert definition hashtable to JSON $policyDefinitionContentJson = (ConvertTo-Json -InputObject $policyDefinitionContent -Depth 100).Replace('\u0027', "'") $formattedPolicyDefinitionContentJson = Format-PolicyDefinitionJson -Json $policyDefinitionContentJson # Write JSON to file $null = Set-Content -Path $filePath -Value $formattedPolicyDefinitionContentJson -Encoding 'UTF8' -Force # Return policy information $result = [PSCustomObject]@{ PSTypeName = 'GuestConfiguration.Policy' Name = $packageName Path = $filePath PolicyId = $PolicyId } return $result } #EndRegion './Public/New-GuestConfigurationPolicy.ps1' 394 #Region './Public/Protect-GuestConfigurationPackage.ps1' 0 <# .SYNOPSIS Signs a Guest Configuration package using either a certificate on Windows or GPG keys on Linux. .PARAMETER Path The path of the Guest Configuration package to sign. .PARAMETER Certificate The 'Code Signing' certificate to sign the package with. This is only supported on Windows. This certificate will need to be installed on machines running this package. See examples for how to generate a test certificate. .PARAMETER PrivateGpgKeyPath The private GPG key path to sign the package with. This is only supported on Linux. See examples for how to generate this key. .PARAMETER PublicGpgKeyPath The public GPG key path to sign the package with. This is only supported on Linux. This key will need to be installed on any machines running this package at the path: /usr/local/share/ca-certificates/gc/pub_keyring.gpg See examples for how to generate this key. .EXAMPLE # Windows # Please note that self-signed certs should not be used in production, only testing # Create a code signing cert $myCert = New-SelfSignedCertificate -Type 'CodeSigningCert' -DnsName 'GCEncryptionCertificate' -HashAlgorithm 'SHA256' # Export the certificates $myPwd = ConvertTo-SecureString -String 'Password1234' -Force -AsPlainText $myCert | Export-PfxCertificate -FilePath 'C:\demo\GCPrivateKey.pfx' -Password $myPwd $myCert | Export-Certificate -FilePath 'C:\demo\GCPublicKey.cer' -Force # Import the certificate Import-PfxCertificate -FilePath 'C:\demo\GCPrivateKey.pfx' -Password $myPwd -CertStoreLocation 'Cert:\LocalMachine\My' # Sign the package $certToSignThePackage = Get-ChildItem -Path cert:\LocalMachine\My | Where-Object {($_.Subject-eq "CN=GCEncryptionCertificate") } Protect-GuestConfigurationPackage -Path C:\demo\AuditWindowsService.zip -Certificate $certToSignThePackage -Verbose .EXAMPLE # Linux # Generate gpg key gpg --gen-key # Export public key gpg --output public.gpg --export <email-id used to generate gpg key> # Export private key gpg --output private.gpg --export-secret-key <email-id used to generate gpg key> # Sign Linux policy package Protect-GuestConfigurationPackage -Path ./not_installed_application_linux.zip -PrivateGpgKeyPath ./private.gpg -PublicGpgKeyPath ./public.gpg -Verbose .OUTPUTS Returns the name of the configuration in the package and the path of the output signed Guest Configuration package. $result = [PSCustomObject]@{ Name = $configurationName Path = $signedPackageFilePath } #> function Protect-GuestConfigurationPackage { [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = "Certificate")] [Parameter(Position = 0, Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = "GpgKeys")] [ValidateNotNullOrEmpty()] [String] $Path, [Parameter(Mandatory = $true, ParameterSetName = "Certificate")] [ValidateNotNullOrEmpty()] [System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate, [Parameter(Mandatory = $true, ParameterSetName = "GpgKeys")] [ValidateNotNullOrEmpty()] [String] $PrivateGpgKeyPath, [Parameter(Mandatory = $true, ParameterSetName = "GpgKeys")] [ValidateNotNullOrEmpty()] [String] $PublicGpgKeyPath ) $os = Get-OSPlatform if ($PSCmdlet.ParameterSetName -eq 'GpgKeys' -and $os -ine 'Linux') { throw 'GPG key signing is only supported on Linux.' } elseif ($PSCmdlet.ParameterSetName -eq 'Certificate' -and $os -ine 'Windows') { throw 'Certificate signing is only supported on Windows.' } $Path = Resolve-RelativePath -Path $Path if (-not (Test-Path $Path -PathType 'Leaf')) { throw "Could not find a file at the path '$Path'" } if ($PSCmdlet.ParameterSetName -eq 'GpgKeys') { $PrivateGpgKeyPath = Resolve-RelativePath -Path $PrivateGpgKeyPath if (-not (Test-Path -Path $PrivateGpgKeyPath)) { throw "Could not find the private GPG key file at the path '$PrivateGpgKeyPath'" } $PublicGpgKeyPath = Resolve-RelativePath -Path $PublicGpgKeyPath if (-not (Test-Path -Path $PublicGpgKeyPath)) { throw "Could not find the public GPG key file at the path '$PublicGpgKeyPath'" } } $package = Get-Item -Path $Path $packageDirectory = $package.Directory $packageFileName = [System.IO.Path]::GetFileNameWithoutExtension($Path) $signedPackageName = "$($packageFileName)_signed.zip" $signedPackageFilePath = Join-Path -Path $packageDirectory -ChildPath $signedPackageName if (Test-Path -Path $signedPackageFilePath) { $null = Remove-Item -Path $signedPackageFilePath -Force } $tempDirectory = Reset-GCWorkerTempDirectory try { # Unzip policy package. $null = Expand-Archive -Path $Path -DestinationPath $tempDirectory # Find and validate the mof file $mofFilePattern = '*.mof' $mofChildItems = @( Get-ChildItem -Path $tempDirectory -Filter $mofFilePattern -File ) if ($mofChildItems.Count -eq 0) { throw "No .mof file found in the package. The Guest Configuration package must include a compiled DSC configuration (.mof) with the same name as the package. Please use the New-GuestConfigurationPackage cmdlet to generate a valid package." } elseif ($mofChildItems.Count -gt 1) { throw "Found more than one .mof file in the extracted Guest Configuration package. Please remove any extra .mof files from the root of the package. Please use the New-GuestConfigurationPackage cmdlet to generate a valid package." } $mofFile = $mofChildItems[0] $configurationName = $mofFile.BaseName if ($PSCmdlet.ParameterSetName -eq 'Certificate') { # Create catalog file $catalogFilePath = Join-Path -Path $tempDirectory -ChildPath "$configurationName.cat" if (Test-Path -Path $catalogFilePath) { $null = Remove-Item -Path $catalogFilePath -Force } Write-Verbose -Message "Creating the catalog file at '$catalogFilePath'" $null = New-FileCatalog -Path $tempDirectory -CatalogFilePath $catalogFilePath -CatalogVersion 2 # Sign catalog file Write-Verbose -Message "Signing the catalog file at '$catalogFilePath'" $codeSignOutput = Set-AuthenticodeSignature -Certificate $Certificate -FilePath $catalogFilePath # Validate that file was signed $signature = Get-AuthenticodeSignature -FilePath $catalogFilePath if ($null -eq $signature.SignerCertificate) { throw $codeSignOutput.StatusMessage } elseif ($signature.SignerCertificate.Thumbprint -ne $Certificate.Thumbprint) { throw $codeSignOutput.StatusMessage } } else { $ascFilePath = Join-Path -Path $tempDirectory -ChildPath "$configurationName.asc" $hashFilePath = Join-Path -Path $tempDirectory -ChildPath "$configurationName.sha256sums" Write-Verbose -Message "Creating hash file at '$hashFilePath'" Push-Location -Path $tempDirectory try { bash -c "find ./ -type f -print0 | xargs -0 sha256sum | grep -v sha256sums > $hashFilePath" } finally { Pop-Location } Write-Verbose -Message "Signing hash file at '$hashFilePath'" gpg --import $PrivateGpgKeyPath gpg --no-default-keyring --keyring $PublicGpgKeyPath --output $ascFilePath --armor --detach-sign $hashFilePath } # Zip the signed Guest Configuration package Write-Verbose -Message "Creating the signed Guest Configuration package at '$signedPackageFilePath'" $archiveSourcePath = Join-Path -Path $tempDirectory -ChildPath '*' $null = Compress-Archive -Path $archiveSourcePath -DestinationPath $signedPackageFilePath $result = [PSCustomObject]@{ Name = $configurationName Path = $signedPackageFilePath } } finally { $null = Reset-GCWorkerTempDirectory } return $result } #EndRegion './Public/Protect-GuestConfigurationPackage.ps1' 233 #Region './Public/Start-GuestConfigurationPackageRemediation.ps1' 0 <# .SYNOPSIS Applies the given Guest Configuration package file (.zip) to the current machine. This will modify your local machine. .PARAMETER Path The path to the Guest Configuration package file (.zip) to apply. .PARAMETER Parameter A list of hashtables describing the parameters to use when running the package. Basic Example: $Parameter = @( @{ ResourceType = 'Service' ResourceId = 'windowsService' ResourcePropertyName = 'Name' ResourcePropertyValue = 'winrm' }, @{ ResourceType = 'Service' ResourceId = 'windowsService' ResourcePropertyName = 'Ensure' ResourcePropertyValue = 'Present' } ) Technical Example: The Guest Configuration agent will replace parameter values in the compiled DSC configuration (.mof) file in the package before running it. If your compiled DSC configuration (.mof) file looked like this: instance of TestFile as $TestFile1ref { ModuleName = "TestFileModule"; ModuleVersion = "1.0.0.0"; ResourceID = "[TestFile]MyTestFile"; <--- This is both the resource type and ID Path = "test.txt"; <--- Here is the name of the parameter that I want to change the value of Content = "default"; Ensure = "Present"; SourceInfo = "TestFileSource"; ConfigurationName = "TestFileConfig"; }; Then your parameter value would look like this: $Parameter = @( @{ ResourceType = 'TestFile' ResourceId = 'MyTestFile' ResourcePropertyName = 'Path' ResourcePropertyValue = 'C:\myPath\newFile.txt' } ) .EXAMPLE Start-GuestConfigurationPackage -Path ./custom_policy/WindowsTLS.zip .EXAMPLE $Parameter = @( @{ ResourceType = 'MyFile' ResourceId = 'hi' ResourcePropertyName = 'Ensure' ResourcePropertyValue = 'Present' } ) Start-GuestConfigurationPackage -Path ./custom_policy/AuditWindowsService.zip -Parameter $Parameter .OUTPUTS Returns a PSCustomObject with the report properties from running the package. Here is an example output: additionalProperties : {} assignmentName : TestFilePackage complianceStatus : True endTime : 5/9/2022 11:42:12 PM jobId : 18df23b4-cd22-4c26-b4b7-85b91873ec41 operationtype : Consistency resources : {@{complianceStatus=True; properties=; reasons=System.Object[]}} startTime : 5/9/2022 11:42:10 PM #> function Start-GuestConfigurationPackageRemediation { [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [System.String] $Path, [Parameter()] [Hashtable[]] $Parameter = @() ) $invokeParameters = @{ Path = $Path Apply = $true } if ($null -ne $Parameter) { $invokeParameters['Parameter'] = $Parameter } $result = Invoke-GuestConfigurationPackage @invokeParameters return $result } #EndRegion './Public/Start-GuestConfigurationPackageRemediation.ps1' 113 # SIG # Begin signature block # MIInrQYJKoZIhvcNAQcCoIInnjCCJ5oCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCFW+LK6mKT984l # RB+ZbQmaqz4QqdxgW/uGEDPwA+jVjaCCDYEwggX/MIID56ADAgECAhMzAAACUosz # qviV8znbAAAAAAJSMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjEwOTAyMTgzMjU5WhcNMjIwOTAxMTgzMjU5WjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQDQ5M+Ps/X7BNuv5B/0I6uoDwj0NJOo1KrVQqO7ggRXccklyTrWL4xMShjIou2I # sbYnF67wXzVAq5Om4oe+LfzSDOzjcb6ms00gBo0OQaqwQ1BijyJ7NvDf80I1fW9O # L76Kt0Wpc2zrGhzcHdb7upPrvxvSNNUvxK3sgw7YTt31410vpEp8yfBEl/hd8ZzA # v47DCgJ5j1zm295s1RVZHNp6MoiQFVOECm4AwK2l28i+YER1JO4IplTH44uvzX9o # RnJHaMvWzZEpozPy4jNO2DDqbcNs4zh7AWMhE1PWFVA+CHI/En5nASvCvLmuR/t8 # q4bc8XR8QIZJQSp+2U6m2ldNAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUNZJaEUGL2Guwt7ZOAu4efEYXedEw # UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1 # ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDY3NTk3MB8GA1UdIwQYMBaAFEhu # ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu # bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w # Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3 # Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx # MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAFkk3 # uSxkTEBh1NtAl7BivIEsAWdgX1qZ+EdZMYbQKasY6IhSLXRMxF1B3OKdR9K/kccp # kvNcGl8D7YyYS4mhCUMBR+VLrg3f8PUj38A9V5aiY2/Jok7WZFOAmjPRNNGnyeg7 # l0lTiThFqE+2aOs6+heegqAdelGgNJKRHLWRuhGKuLIw5lkgx9Ky+QvZrn/Ddi8u # TIgWKp+MGG8xY6PBvvjgt9jQShlnPrZ3UY8Bvwy6rynhXBaV0V0TTL0gEx7eh/K1 # o8Miaru6s/7FyqOLeUS4vTHh9TgBL5DtxCYurXbSBVtL1Fj44+Od/6cmC9mmvrti # yG709Y3Rd3YdJj2f3GJq7Y7KdWq0QYhatKhBeg4fxjhg0yut2g6aM1mxjNPrE48z # 6HWCNGu9gMK5ZudldRw4a45Z06Aoktof0CqOyTErvq0YjoE4Xpa0+87T/PVUXNqf # 7Y+qSU7+9LtLQuMYR4w3cSPjuNusvLf9gBnch5RqM7kaDtYWDgLyB42EfsxeMqwK # WwA+TVi0HrWRqfSx2olbE56hJcEkMjOSKz3sRuupFCX3UroyYf52L+2iVTrda8XW # esPG62Mnn3T8AuLfzeJFuAbfOSERx7IFZO92UPoXE1uEjL5skl1yTZB3MubgOA4F # 8KoRNhviFAEST+nG8c8uIsbZeb08SeYQMqjVEmkwggd6MIIFYqADAgECAgphDpDS # AAAAAAADMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMK # V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0 # IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0 # ZSBBdXRob3JpdHkgMjAxMTAeFw0xMTA3MDgyMDU5MDlaFw0yNjA3MDgyMTA5MDla # MH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS # ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMT # H01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTEwggIiMA0GCSqGSIb3DQEB # AQUAA4ICDwAwggIKAoICAQCr8PpyEBwurdhuqoIQTTS68rZYIZ9CGypr6VpQqrgG # OBoESbp/wwwe3TdrxhLYC/A4wpkGsMg51QEUMULTiQ15ZId+lGAkbK+eSZzpaF7S # 35tTsgosw6/ZqSuuegmv15ZZymAaBelmdugyUiYSL+erCFDPs0S3XdjELgN1q2jz # y23zOlyhFvRGuuA4ZKxuZDV4pqBjDy3TQJP4494HDdVceaVJKecNvqATd76UPe/7 # 4ytaEB9NViiienLgEjq3SV7Y7e1DkYPZe7J7hhvZPrGMXeiJT4Qa8qEvWeSQOy2u # M1jFtz7+MtOzAz2xsq+SOH7SnYAs9U5WkSE1JcM5bmR/U7qcD60ZI4TL9LoDho33 # X/DQUr+MlIe8wCF0JV8YKLbMJyg4JZg5SjbPfLGSrhwjp6lm7GEfauEoSZ1fiOIl # XdMhSz5SxLVXPyQD8NF6Wy/VI+NwXQ9RRnez+ADhvKwCgl/bwBWzvRvUVUvnOaEP # 6SNJvBi4RHxF5MHDcnrgcuck379GmcXvwhxX24ON7E1JMKerjt/sW5+v/N2wZuLB # l4F77dbtS+dJKacTKKanfWeA5opieF+yL4TXV5xcv3coKPHtbcMojyyPQDdPweGF # RInECUzF1KVDL3SV9274eCBYLBNdYJWaPk8zhNqwiBfenk70lrC8RqBsmNLg1oiM # CwIDAQABo4IB7TCCAekwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFEhuZOVQ # BdOCqhc3NyK1bajKdQKVMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1Ud # DwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFHItOgIxkEO5FAVO # 4eqnxzHRI4k0MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwubWljcm9zb2Z0 # LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y # Mi5jcmwwXgYIKwYBBQUHAQEEUjBQME4GCCsGAQUFBzAChkJodHRwOi8vd3d3Lm1p # Y3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y # Mi5jcnQwgZ8GA1UdIASBlzCBlDCBkQYJKwYBBAGCNy4DMIGDMD8GCCsGAQUFBwIB # FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2RvY3MvcHJpbWFyeWNw # cy5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AcABvAGwAaQBjAHkA # XwBzAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAGfyhqWY # 4FR5Gi7T2HRnIpsLlhHhY5KZQpZ90nkMkMFlXy4sPvjDctFtg/6+P+gKyju/R6mj # 82nbY78iNaWXXWWEkH2LRlBV2AySfNIaSxzzPEKLUtCw/WvjPgcuKZvmPRul1LUd # d5Q54ulkyUQ9eHoj8xN9ppB0g430yyYCRirCihC7pKkFDJvtaPpoLpWgKj8qa1hJ # Yx8JaW5amJbkg/TAj/NGK978O9C9Ne9uJa7lryft0N3zDq+ZKJeYTQ49C/IIidYf # wzIY4vDFLc5bnrRJOQrGCsLGra7lstnbFYhRRVg4MnEnGn+x9Cf43iw6IGmYslmJ # aG5vp7d0w0AFBqYBKig+gj8TTWYLwLNN9eGPfxxvFX1Fp3blQCplo8NdUmKGwx1j # NpeG39rz+PIWoZon4c2ll9DuXWNB41sHnIc+BncG0QaxdR8UvmFhtfDcxhsEvt9B # xw4o7t5lL+yX9qFcltgA1qFGvVnzl6UJS0gQmYAf0AApxbGbpT9Fdx41xtKiop96 # eiL6SJUfq/tHI4D1nvi/a7dLl+LrdXga7Oo3mXkYS//WsyNodeav+vyL6wuA6mk7 # r/ww7QRMjt/fdW1jkT3RnVZOT7+AVyKheBEyIXrvQQqxP/uozKRdwaGIm1dxVk5I # RcBCyZt2WwqASGv9eZ/BvW1taslScxMNelDNMYIZgjCCGX4CAQEwgZUwfjELMAkG # A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx # HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z # b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAlKLM6r4lfM52wAAAAACUjAN # BglghkgBZQMEAgEFAKCBrjAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor # BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgu3wXCf7O # 2sglDXDYTM+Kxf6GZJPS1qiqvrzUslkOrc0wQgYKKwYBBAGCNwIBDDE0MDKgFIAS # AE0AaQBjAHIAbwBzAG8AZgB0oRqAGGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbTAN # BgkqhkiG9w0BAQEFAASCAQCbXorxJ/cs8eV30IrDaklbebT25fWj/9pSrDhLSy9V # U0bZ/9lzXSeEF+lucmx42jJhfSno+fgE7G+wZJCvKifGkX/qnkY+/ddBfWCJ6XAU # YIPj2QVG9fz5L4O/DiE0F9K7BgyOhPUzowRtZN38EHpeZM7+9MKkM0EU3Zu/tnxy # iiaY2kXfEqYfeJOnRv7+pzD13oyErTDJwtg4gQ3s2AWmgyI1Rau6wBRkY/D40gIL # 8WznMPnrjR+93dur0npKTRzUpnW/PF7F6Mzke12Q68XTCDdEf6mtQxmGD4U621QN # 3NFEQrpnDfPMZZZzpHnYmSN2PG2al06GiLzqQ3cihKNUoYIXDDCCFwgGCisGAQQB # gjcDAwExghb4MIIW9AYJKoZIhvcNAQcCoIIW5TCCFuECAQMxDzANBglghkgBZQME # AgEFADCCAVUGCyqGSIb3DQEJEAEEoIIBRASCAUAwggE8AgEBBgorBgEEAYRZCgMB # MDEwDQYJYIZIAWUDBAIBBQAEIDfAyMCHrZznK0tnmEbTfFLrRiDQ8yvcsLF1H18H # IOswAgZihKvdkqkYEzIwMjIwNTI0MjE1ODA0LjIyNFowBIACAfSggdSkgdEwgc4x # CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt # b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1p # Y3Jvc29mdCBPcGVyYXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMg # VFNTIEVTTjo0NjJGLUUzMTktM0YyMDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUt # U3RhbXAgU2VydmljZaCCEV8wggcQMIIE+KADAgECAhMzAAABpAfP44+jum/WAAEA # AAGkMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo # aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y # cG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEw # MB4XDTIyMDMwMjE4NTExOFoXDTIzMDUxMTE4NTExOFowgc4xCzAJBgNVBAYTAlVT # MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK # ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVy # YXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjo0NjJG # LUUzMTktM0YyMDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vydmlj # ZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMBHjgD6FPy81PUhcOIV # Gh4bOSaq634Y+TjW2hNF9BlnWxLJCEuMiV6YF5x6YTM7T1ZLM6NnH0whPypiz3bV # ZRmwgGyTURKfVyPJ89R3WaZ/HMvcAJZnCMgL+mOpxE94gwQJD/qo8UquOrCKCY/f # cjchxV8yMkfIqP69HnWfW0ratk+I2GZF2ISFyRtvEuxJvacIFDFkQXj3H+Xy9IHz # Nqqi+g54iQjOAN6s3s68mi6rqv6+D9DPVPg1ev6worI3FlYzrPLCIunsbtYt3Xw3 # aHKMfA+SH8CV4iqJ/eEZUP1uFJT50MAPNQlIwWERa6cccSVB5mN2YgHf8zDUqQU4 # k2/DWw+14iLkwrgNlfdZ38V3xmxC9mZc9YnwFc32xi0czPzN15C8wiZEIqCddxbw # imc+0LtPKandRXk2hMfwg0XpZaJxDfLTgvYjVU5PXTgB10mhWAA/YosgbB8KzvAx # XPnrEnYg3XLWkgBZ+lOrHvqiszlFCGQC9rKPVFPCCsey356VhfcXlvwAJauAk7V0 # nLVTgwi/5ILyHffEuZYDnrx6a+snqDTHL/ZqRsB5HHq0XBo/i7BVuMXnSSXlFCo3 # On8IOl8JOKQ4CrIlri9qWJYMxsSICscotgODoYOO4lmXltKOB0l0IAhEXwSSKID5 # QAa9wTpIagea2hzjI6SUY1W/AgMBAAGjggE2MIIBMjAdBgNVHQ4EFgQU4tATn6z4 # CBL2xZQd0jjN6SnjJMIwHwYDVR0jBBgwFoAUn6cVXQBeYl2D9OXSZacbUzUZ6XIw # XwYDVR0fBFgwVjBUoFKgUIZOaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9w # cy9jcmwvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3Js # MGwGCCsGAQUFBwEBBGAwXjBcBggrBgEFBQcwAoZQaHR0cDovL3d3dy5taWNyb3Nv # ZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENB # JTIwMjAxMCgxKS5jcnQwDAYDVR0TAQH/BAIwADATBgNVHSUEDDAKBggrBgEFBQcD # CDANBgkqhkiG9w0BAQsFAAOCAgEACVYcUNEMlyTuPDBGhiZ1U548ssF6J2g9QElW # Eb2cZ4dL0+5G8721/giRtTPvgxQhDF5rJCjHGj8nFSqOE8fnYz9vgb2YclYHvkoK # WUJODxjhWS+S06ZLR/nDS85HeDAD0FGduAA80Q7vGzknKW2jxoNHTb74KQEMWiUK # 1M2PDN+eISPXPhPudGVGLbIEAk1Goj5VjzbQuLKhm2Tk4a22rkXkeE98gyNojHlB # hHbb7nex3zGBTBGkVtwt2ud7qN2rcpuJhsJ/vL/0XYLtyOk7eSQZdfye0TT1/qj1 # 8iSXHsIXDhHOuTKqBiiatoo4Unwk7uGyM0lv38Ztr+YpajSP+p0PEMRH9RdfrKRm # 4bHV5CmOTIzAmc49YZt40hhlVwlClFA4M+zn3cyLmEGwfNqD693hD5W3vcpnhf3x # hZbVWTVpJH1CPGTmR4y5U9kxwysK8VlfCFRwYUa5640KsgIv1tJhF9LXemWIPEnu # w9JnzHZ3iSw5dbTSXp9HmdOJIzsO+/tjQwZWBSFqnayaGv3Y8w1KYiQJS8cKJhwn # hGgBPbyan+E5D9TyY9dKlZ3FikstwM4hKYGEUlg3tqaWEilWwa9SaNetNxjSfgah # 782qzbjTQhwDgc6Jf07F2ak0YMnNJFHsBb1NPw77dhmo9ki8vrLOB++d6Gm2Z/jD # pDOSst8wggdxMIIFWaADAgECAhMzAAAAFcXna54Cm0mZAAAAAAAVMA0GCSqGSIb3 # DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G # A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIw # MAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAx # MDAeFw0yMTA5MzAxODIyMjVaFw0zMDA5MzAxODMyMjVaMHwxCzAJBgNVBAYTAlVT # MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK # ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1l # LVN0YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA # 5OGmTOe0ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51yMo1V/YBf2xK4OK9uT4XYDP/ # XE/HZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY6GB9alKDRLemjkZrBxTzxXb1 # hlDcwUTIcVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9cmmvHaus9ja+NSZk2pg7uhp7 # M62AW36MEBydUv626GIl3GoPz130/o5Tz9bshVZN7928jaTjkY+yOSxRnOlwaQ3K # Ni1wjjHINSi947SHJMPgyY9+tVSP3PoFVZhtaDuaRr3tpK56KTesy+uDRedGbsoy # 1cCGMFxPLOJiss254o2I5JasAUq7vnGpF1tnYN74kpEeHT39IM9zfUGaRnXNxF80 # 3RKJ1v2lIH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2K26oElHovwUDo9Fzpk03dJQc # NIIP8BDyt0cY7afomXw/TNuvXsLz1dhzPUNOwTM5TI4CvEJoLhDqhFFG4tG9ahha # YQFzymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZki1ugpoMhXV8wdJGUlNi5UPkL # iWHzNgY1GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9QBXpsxREdcu+N+VLEhReTwDwV # 2xo3xwgVGD94q0W29R6HXtqPnhZyacaue7e3PmriLq0CAwEAAaOCAd0wggHZMBIG # CSsGAQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUCBBYEFCqnUv5kxJq+gpE8RjUp # zxD/LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJlpxtTNRnpcjBcBgNVHSAEVTBT # MFEGDCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jv # c29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5odG0wEwYDVR0lBAwwCgYI # KwYBBQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGG # MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/oolxiaNE9lJBb186a # GMQwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3Br # aS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3JsMFoGCCsG # AQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29t # L3BraS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcnQwDQYJKoZIhvcN # AQELBQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/ypb+pcFLY+TkdkeLEGk5c9MTO1 # OdfCcTY/2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulmZzpTTd2YurYeeNg2LpypglYA # A7AFvonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM9W0jVOR4U3UkV7ndn/OOPcbz # aN9l9qRWqveVtihVJ9AkvUCgvxm2EhIRXT0n4ECWOKz3+SmJw7wXsFSFQrP8DJ6L # GYnn8AtqgcKBGUIZUnWKNsIdw2FzLixre24/LAl4FOmRsqlb30mjdAy87JGA0j3m # Sj5mO0+7hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3UwxTSwethQ/gpY3UA8x1RtnWN0 # SCyxTkctwRQEcb9k+SS+c23Kjgm9swFXSVRk2XPXfx5bRAGOWhmRaw2fpCjcZxko # JLo4S5pu+yFUa2pFEUep8beuyOiJXk+d0tBMdrVXVAmxaQFEfnyhYWxz/gq77EFm # PWn9y8FBSX5+k77L+DvktxW/tM4+pTFRhLy/AsGConsXHRWJjXD+57XQKBqJC482 # 2rpM+Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU5nR0W2rRnj7tfqAxM328y+l7 # vzhwRNGQ8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEGahC0HVUzWLOhcGbyoYIC0jCC # AjsCAQEwgfyhgdSkgdEwgc4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5n # dG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9y # YXRpb24xKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1ZXJ0byBSaWNv # MSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjo0NjJGLUUzMTktM0YyMDElMCMGA1UE # AxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsOAwIaAxUA # NBwo4pNrfEL6DVo+tw96vGJvLp+ggYMwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEG # A1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWlj # cm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFt # cCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIFAOY3u+8wIhgPMjAyMjA1MjUwMDE3 # NTFaGA8yMDIyMDUyNjAwMTc1MVowdzA9BgorBgEEAYRZCgQBMS8wLTAKAgUA5je7 # 7wIBADAKAgEAAgIRuwIB/zAHAgEAAgIQczAKAgUA5jkNbwIBADA2BgorBgEEAYRZ # CgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYagMA0G # CSqGSIb3DQEBBQUAA4GBACB0APot6n01f/CJpxZljAJJKDdbENbq2AyEE3w86o/K # O3+31ZURVM1/H0A1rDGg33h52rJXo8bdbcQ3Bj2YN/aXOlVD8Uv51stj5O5jEUnh # S/7A1Wuh/CDAoMhvNawM8kHpmbdySmct+4TkFvH1DwoLED5Fw9bDjzXErgxPZiY/ # MYIEDTCCBAkCAQEwgZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0 # b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3Jh # dGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMA # AAGkB8/jj6O6b9YAAQAAAaQwDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJ # AzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQg94b4tItTyOnUxxOG7T5s # 0E+n4VG32zjBga44zEtKbwwwgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCAF # /OCjISZwpMBJ8MJ3WwMCF3qOa5YHFG6J4uHjaup5+DCBmDCBgKR+MHwxCzAJBgNV # BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4w # HAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29m # dCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAABpAfP44+jum/WAAEAAAGkMCIEIF+b # HcIzBvIxCIlj3c9Bb0Kb5fUwIguFjW8FnSwfNH9dMA0GCSqGSIb3DQEBCwUABIIC # AH3/pqlXdtG3Q0P4scmb33z5x2Rwc+VJ5PEOKt21AblVJvLAYyzFNCY3qzcc8lbE # 8JdP3VTAN+MGjAHWgj4tOAWUKT6homtNI41D0XH+Z30SyXiB3sk3PFOrB5YYN3bW # aq1yaGBYGUS5SiPXQp0fOGgrj4cUPzA7MxvQJSxip0+ubBhHnKtE7Iw3GqZ0k6R0 # lmiNSnoT020A+788M8JycuC31+QbDbj/pEQM7w4bN+S/mGb4XerDACxNTETrGwbL # 7tskG0/l1VhoOWD4uMCbctFJ/pFtPRrfOg6IgHAG+9M7LDZkWCodxL6MXQCfFKC9 # gR/u9vIiN0JsllmWVQEMkMAw+gl1XXanJxLNCzuv6JbP/T7yoemrcdyOF0/d4VN/ # yA9wolGcUMgZJ0X/k0D6fAoyizKK6D+9gMFz6oo9i7qZ9ZZuZ1thBwUHdsnEdMdL # FNX0BWtCp+mxLSG+/LxnFXB0hU9OYCzLCTOHpNJTR2lxx7ZKdeXbOWjZuaRo9OYF # Gzx3WhHpqf81Yi/gp73p5iNJvQUcblPtGobhFKYg8QWaps+p18GDhvFOuEdALZH4 # Ex0Tv7DNdEcIwLchDTUYS3paBUIkkIooiIP7tI3TC6Z6F7jtmqaftAJut4MT4r8E # 46zuPhtRGLSNJfRiHpZ4JxWGS5sUz+uH2PoEo2MN3ZZs # SIG # End signature block |