GuestConfiguration.psm1
using namespace System.IO.Compression.ZipFile #Region './prefix.ps1' 0 Set-StrictMode -Version latest $ErrorActionPreference = 'Stop' Import-Module $PSScriptRoot/Modules/GuestConfigPath -Force Import-Module $PSScriptRoot/Modules/DscOperations -Force Import-Module $PSScriptRoot/Modules/GuestConfigurationPolicy -Force Import-LocalizedData -BaseDirectory $PSScriptRoot -FileName GuestConfiguration.psd1 -BindingVariable GuestConfigurationManifest if ($IsLinux -and ( $PSVersionTable.PSVersion.Major -lt 7 -or ($PSVersionTable.PSVersion.Major -eq 7 -and $PSVersionTable.PSVersion.Minor -lt 2) )) { throw 'The Linux agent requires at least PowerShell v7.2.preview.6 to support the DSC subsystem.' } $currentCulture = [System.Globalization.CultureInfo]::CurrentCulture if (($currentCulture.Name -eq 'en-US-POSIX') -and ($(Get-OSPlatform) -eq 'Linux')) { Write-Warning "'$($currentCulture.Name)' Culture is not supported, changing it to 'en-US'" # Set Culture info to en-US [System.Globalization.CultureInfo]::CurrentUICulture = [System.Globalization.CultureInfo]::new('en-US') [System.Globalization.CultureInfo]::CurrentCulture = [System.Globalization.CultureInfo]::new('en-US') } #inject version info to GuestConfigPath.psm1 InitReleaseVersionInfo $GuestConfigurationManifest.moduleVersion #EndRegion './prefix.ps1' 28 #Region './Enum/AssignmentType.ps1' 0 enum AssignmentType { ApplyAndAutoCorrect ApplyAndMonitor Audit } #EndRegion './Enum/AssignmentType.ps1' 7 #Region './Enum/PackageType.ps1' 0 enum PackageType { Audit AuditAndSet } #EndRegion './Enum/PackageType.ps1' 6 #Region './Private/Compress-ArchiveWithPermissions.ps1' 0 #using namespace System.IO.Compression.ZipFile <# .SYNOPSIS Creates a zip file without wiping out the item type and permissions. .DESCRIPTION The Compress-Archive and Expand-Archive cmdlets wipe out the file modes on Linux. This erroneously removes file permissions and the bit that determines whether the item is a file or a directory. We have created this function to avoid the errors in those cmdlets on Linux. The issue has been filed with PowerShell here: https://github.com/PowerShell/Microsoft.PowerShell.Archive/issues/36 .PARAMETER Path The path of the directory to compress. .PARAMETER DestinationPath The destination path to compress the directory to. .PARAMETER CompressionLevel The compression level. The options are Fastest, Optimal, and NoCompression. The default value is Fastest. .PARAMETER IncludeBaseDirectory If specified the base directory will be included in the zip file. .EXAMPLE Compress-ArchiveWithPermissions -Path C:\MyDir -DestinationPath C:\MyDir.zip #> function Compress-ArchiveWithPermissions { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String] $Path, [Parameter(Mandatory = $true)] [System.String] $DestinationPath, [Parameter()] [System.IO.Compression.CompressionLevel] $CompressionLevel = [System.IO.Compression.CompressionLevel]::Fastest, [Parameter()] [System.Management.Automation.SwitchParameter] $IncludeBaseDirectory ) Write-Verbose -Message "Compressing from '$Path' to '$DestinationPath' with compression level '$CompressionLevel'" [System.IO.Compression.ZipFile]::CreateFromDirectory($Path, $DestinationPath, $CompressionLevel, $IncludeBaseDirectory.ToBool()) } #EndRegion './Private/Compress-ArchiveWithPermissions.ps1' 59 #Region './Private/Edit-ChefInSpecMofContent.ps1' 0 function Edit-ChefInSpecMofContent { [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) $content = '' $resourceCount = 0 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 } } $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 $MofPath -Value $content -Force } #EndRegion './Private/Edit-ChefInSpecMofContent.ps1' 71 #Region './Private/Expand-ArchiveWithPermissions.ps1' 0 #using namespace System.IO.Compression.ZipFile <# .SYNOPSIS Creates a zip file without wiping out the item type and permissions. .DESCRIPTION The Compress-Archive and Expand-Archive cmdlets wipe out the file modes on Linux. This erroneously removes file permissions and the bit that determines whether the item is a file or a directory. We have created this function to avoid the errors in those cmdlets on Linux. The issue has been filed with PowerShell here: https://github.com/PowerShell/Microsoft.PowerShell.Archive/issues/36 .PARAMETER Path The path of the directory to compress. .PARAMETER DestinationPath The destination path to compress the directory to. .PARAMETER Force If specified, any existing files at the destination path will be removed. .EXAMPLE Expand-ArchiveWithPermissions -Path C:\MyDir.zip -DestinationPath C:\MyDir -Force #> function Expand-ArchiveWithPermissions { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String] $Path, [Parameter(Mandatory = $true)] [System.String] $DestinationPath, [Parameter()] [Switch] $Force ) Write-Verbose -Message "Expanding from '$Path' to '$DestinationPath' with force as $Force" [System.IO.Compression.ZipFile]::ExtractToDirectory($Path, $DestinationPath, $Force.ToBool()) } #EndRegion './Private/Expand-ArchiveWithPermissions.ps1' 50 #Region './Private/Get-GuestConfigurationPackageFromUri.ps1' 0 function Get-GuestConfigurationPackageFromUri { [CmdletBinding()] [OutputType([System.Io.FileInfo])] param ( [Parameter()] [Uri] [ValidateScript({([Uri]$_).Scheme -match '^http'})] [Alias('Url')] $Uri ) # Abstracting this in another function as we may want to support Proxy later. $tempFileName = [io.path]::GetTempFileName() $null = [System.Net.WebClient]::new().DownloadFile($Uri, $tempFileName) # The zip can be PackageName_0.2.3.zip, so we really need to look at the MOF to find its name. $packageName = Get-GuestConfigurationPackageNameFromZip -Path $tempFileName Move-Item -Path $tempFileName -Destination ('{0}.zip' -f $packageName) -Force -PassThru } #EndRegion './Private/Get-GuestConfigurationPackageFromUri.ps1' 23 #Region './Private/Get-GuestConfigurationPackageMetaConfig.ps1' 0 function Get-GuestConfigurationPackageMetaConfig { [CmdletBinding()] [OutputType([Hashtable])] param ( [Parameter(Mandatory = $true)] [System.String] $Path ) $packageName = Get-GuestConfigurationPackageName -Path $Path $metadataFileName = '{0}.metaconfig.json' -f $packageName $metadataFile = Join-Path -Path $Path -ChildPath $metadataFileName if (Test-Path -Path $metadataFile) { Write-Debug -Message "Loading metadata from meta config file '$metadataFile'." $metadata = Get-Content -raw -Path $metadataFile | ConvertFrom-Json -AsHashtable -ErrorAction Stop } else { $metadata = @{} } #region Extra meta file until Agent supports one unique metadata file $extraMetadataFileName = 'extra.{0}' -f $metadataFileName $extraMetadataFile = Join-Path -Path $Path -ChildPath $extraMetadataFileName if (Test-Path -Path $extraMetadataFile) { Write-Debug -Message "Loading extra metadata from extra meta file '$extraMetadataFile'." $extraMetadata = Get-Content -raw -Path $extraMetadataFile | ConvertFrom-Json -AsHashtable -ErrorAction Stop foreach ($extraKey in $extraMetadata.keys) { if (-not $metadata.ContainsKey($extraKey)) { $metadata[$extraKey] = $extraMetadata[$extraKey] } else { Write-Verbose -Message "The metadata '$extraKey' is already defined in '$metadataFile'." } } } #endregion return $metadata } #EndRegion './Private/Get-GuestConfigurationPackageMetaConfig.ps1' 51 #Region './Private/Get-GuestConfigurationPackageMetadataFromZip.ps1' 0 function Get-GuestConfigurationPackageMetadataFromZip { [CmdletBinding()] [OutputType([PSObject])] param ( [Parameter(Mandatory = $true)] [System.Io.FileInfo] $Path ) $Path = [System.IO.Path]::GetFullPath($Path) # Get Absolute path as .Net methods don't like relative paths. try { $tempFolderPackage = Join-Path -Path ([io.path]::GetTempPath()) -ChildPath ([guid]::NewGuid().Guid) Expand-Archive -LiteralPath $Path -DestinationPath $tempFolderPackage -Force Get-GuestConfigurationPackageMetaConfig -Path $tempFolderPackage } finally { # Remove the temporarily extracted package Remove-Item -Force -Recurse $tempFolderPackage -ErrorAction SilentlyContinue } } #EndRegion './Private/Get-GuestConfigurationPackageMetadataFromZip.ps1' 26 #Region './Private/Get-GuestConfigurationPackageName.ps1' 0 function Get-GuestConfigurationPackageName { [CmdletBinding()] [OutputType([string])] param ( [Parameter(Mandatory = $true)] [System.Io.FileInfo] $Path ) $Path = [System.IO.Path]::GetFullPath($Path) # Get Absolute path as .Net method don't like relative paths. # Make sure we only get the MOF which is at the root of the package $mofFile = @() + (Get-ChildItem -Path (Join-Path -Path $Path -ChildPath *.mof) -File -ErrorAction Stop) if ($mofFile.Count -ne 1) { throw "Invalid GuestConfiguration Package at '$Path'. Found $($mofFile.Count) mof files." return } else { Write-Debug -Message "Found the MOF '$($moffile)' in $Path." } return ([System.Io.Path]::GetFileNameWithoutExtension($mofFile[0])) } #EndRegion './Private/Get-GuestConfigurationPackageName.ps1' 28 #Region './Private/Get-GuestConfigurationPackageNameFromZip.ps1' 0 function Get-GuestConfigurationPackageNameFromZip { [CmdletBinding()] [OutputType([string])] param ( [Parameter(Mandatory = $true)] [System.Io.FileInfo] $Path ) $Path = [System.IO.Path]::GetFullPath($Path) # Get Absolute path as .Net method don't like relative paths. try { $zipRead = [IO.Compression.ZipFile]::OpenRead($Path) # Make sure we only get the MOF which is at the root of the package $mofFile = @() + $zipRead.Entries.FullName.Where({((Split-Path -Leaf -Path $_) -eq $_) -and $_ -match '\.mof$'}) } finally { # Close the zip so we can move it. $zipRead.Dispose() } if ($mofFile.count -ne 1) { throw "Invalid policy package, failed to find unique dsc document in policy package downloaded from '$Uri'." } return ([System.Io.Path]::GetFileNameWithoutExtension($mofFile[0])) } #EndRegion './Private/Get-GuestConfigurationPackageNameFromZip.ps1' 32 #Region './Private/Get-ModuleDependencies.ps1' 0 function Get-ModuleDependencies { [CmdletBinding()] [OutputType([Hashtable[]])] param ( [Parameter(Mandatory = $true)] [String] $ModuleName, [Parameter()] [String] $ModuleVersion ) $moduleDependencies = @() if ($ModuleName -ieq 'PSDesiredStateConfiguration') { throw "Found a dependency on 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 } } $sourceModule = Get-Module @getModuleParameters 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' 96 #Region './Private/Get-ResouceDependenciesFromMof.ps1' 0 function Get-ResouceDependenciesFromMof { [CmdletBinding()] [OutputType([Hashtable[]])] param ( [Parameter(Mandatory = $true)] [String] $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-ResouceDependenciesFromMof.ps1' 40 #Region './Private/Update-GuestConfigurationPackageMetaconfig.ps1' 0 function Update-GuestConfigurationPackageMetaconfig { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String] $MetaConfigPath, [Parameter(Mandatory = $true)] [System.String] $Key, [Parameter(Mandatory = $true)] [System.String] $Value ) $metadataFile = $MetaConfigPath #region Write extra metadata on different file until the GC Agents supports it if ($Key -notin @('debugMode','ConfigurationModeFrequencyMins','configurationMode')) { $fileName = Split-Path -Path $MetadataFile -Leaf $filePath = Split-Path -Path $MetadataFile -Parent $metadataFileName = 'extra.{0}' -f $fileName $metadataFile = Join-Path -Path $filePath -ChildPath $metadataFileName } #endregion Write-Debug -Message "Updating the file '$metadataFile' with key $Key = '$Value'." if (Test-Path -Path $metadataFile) { $metaConfigObject = Get-Content -Raw -Path $metadataFile | ConvertFrom-Json -AsHashtable $metaConfigObject[$Key] = $Value $metaConfigObject | ConvertTo-Json | Out-File -Path $metadataFile -Encoding ascii -Force } else { @{ $Key = $Value } | ConvertTo-Json | Out-File -Path $metadataFile -Encoding ascii -Force } } #EndRegion './Private/Update-GuestConfigurationPackageMetaconfig.ps1' 47 #Region './Public/Get-GuestConfigurationPackageComplianceStatus.ps1' 0 function Get-GuestConfigurationPackageComplianceStatus { [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [System.String] $Path, [Parameter(ValueFromPipelineByPropertyName = $true)] [Hashtable[]] $Parameter = @() ) begin { # Determine if verbose is enabled to pass down to other functions $verbose = ($PSBoundParameters.ContainsKey("Verbose") -and ($PSBoundParameters["Verbose"] -eq $true)) $systemPSModulePath = [Environment]::GetEnvironmentVariable("PSModulePath", "Process") $gcBinPath = Get-GuestConfigBinaryPath $guestConfigurationPolicyPath = Get-GuestConfigPolicyPath } process { try { if ($PSBoundParameters.ContainsKey('Force') -and $Force) { $withForce = $true } else { $withForce = $false } $packagePath = Install-GuestConfigurationPackage -Path $Path -Force:$withForce Write-Debug -Message "Looking into Package '$PackagePath' for MOF document." $packageName = Get-GuestConfigurationPackageName -Path $PackagePath # Confirm mof exists $packageMof = Join-Path -Path $packagePath -ChildPath "$packageName.mof" $dscDocument = Get-Item -Path $packageMof -ErrorAction 'SilentlyContinue' if (-not $dscDocument) { throw "Invalid Guest Configuration package, failed to find dsc document at '$packageMof' path." } # update configuration parameters if ($Parameter.Count -gt 0) { Update-MofDocumentParameters -Path $dscDocument.FullName -Parameter $Parameter } # Publish policy package Publish-DscConfiguration -ConfigurationName $packageName -Path $PackagePath -Verbose:$verbose # Set LCM settings to force load powershell module. $metaConfigPath = Join-Path -Path $PackagePath -ChildPath "$packageName.metaconfig.json" Update-GuestConfigurationPackageMetaconfig -metaConfigPath $metaConfigPath -Key 'debugMode' -Value 'ForceModuleImport' Set-DscLocalConfigurationManager -ConfigurationName $packageName -Path $PackagePath -Verbose:$verbose # Clear Inspec profiles Remove-Item -Path $(Get-InspecProfilePath) -Recurse -Force -ErrorAction SilentlyContinue $getResult = @() $getResult = $getResult + (Get-DscConfiguration -ConfigurationName $packageName -Verbose:$verbose) return $getResult } finally { $env:PSModulePath = $systemPSModulePath } } } #EndRegion './Public/Get-GuestConfigurationPackageComplianceStatus.ps1' 83 #Region './Public/Install-GuestConfigurationAgent.ps1' 0 function Install-GuestConfigurationAgent { [CmdletBinding()] [OutputType([void])] param ( [Parameter()] [System.Management.Automation.SwitchParameter] $Force ) # Unzip Guest Configuration binaries $gcBinPath = Get-GuestConfigBinaryPath $gcBinRootPath = Get-GuestConfigBinaryRootPath $OsPlatform = Get-OSPlatform if ($PSBoundParameters.ContainsKey('Force') -and $PSBoundParameters['Force']) { $withForce = $true } else { $withForce = $false } if ((-not (Test-Path -Path $gcBinPath)) -or $withForce) { # Clean the bin folder Write-Verbose -Message "Removing existing installation from '$gcBinRootPath'." Remove-Item -Path $gcBinRootPath'\*' -Recurse -Force -ErrorAction SilentlyContinue $zippedBinaryPath = Join-Path -Path $(Get-GuestConfigurationModulePath) -ChildPath 'bin' if ($OsPlatform -eq 'Windows') { $zippedBinaryPath = Join-Path -Path $zippedBinaryPath -ChildPath 'DSC_Windows.zip' } else { # Linux zip package contains an additional DSC folder # Remove DSC folder from binary path to avoid two nested DSC folders. $null = New-Item -ItemType Directory -Force -Path $gcBinPath $gcBinPath = (Get-Item -Path $gcBinPath).Parent.FullName $zippedBinaryPath = Join-Path $zippedBinaryPath 'DSC_Linux.zip' } Write-Verbose -Message "Extracting '$zippedBinaryPath' to '$gcBinPath'." [System.IO.Compression.ZipFile]::ExtractToDirectory($zippedBinaryPath, $gcBinPath) if ($OsPlatform -ne 'Windows') { # Fix for “LTTng-UST: Error (-17) while registering tracepoint probe. Duplicate registration of tracepoint probes having the same name is not allowed.” Get-ChildItem -Path $gcBinPath -Filter libcoreclrtraceptprovider.so -Recurse | ForEach-Object { Remove-Item $_.FullName -Force -ErrorAction SilentlyContinue } Get-ChildItem -Path $gcBinPath -Filter *.sh -Recurse | ForEach-Object -Process { chmod @('+x', $_.FullName) } } # Save config file $gcConfigPath = Join-Path (Get-GuestConfigBinaryPath) 'gc.config' '{ "SaveLogsInJsonFormat": true, "DoNotSendReport": true}' | Out-File -Path $gcConfigPath -Encoding ascii -Force if ($OsPlatform -ne 'Windows') { # Give root user permission to execute gc_worker chmod 700 (Get-GuestConfigWorkerBinaryPath) } } else { Write-Verbose -Message "Guest Configuration Agent binaries already installed at '$gcBinPath', skipping." } } #EndRegion './Public/Install-GuestConfigurationAgent.ps1' 75 #Region './Public/Install-GuestConfigurationPackage.ps1' 0 <# .SYNOPSIS Installs a Guest Configuration policy package. .Parameter Package Path or Uri of the Guest Configuration package zip. .Parameter Force Force installing over an existing package, even if it already exists. .Example Install-GuestConfigurationPackage -Path ./custom_policy/WindowsTLS.zip Install-GuestConfigurationPackage -Path ./custom_policy/AuditWindowsService.zip -Force .OUTPUTS The path to the installed Guest Configuration package. #> function Install-GuestConfigurationPackage { [CmdletBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [System.String] $Path, [Parameter(ValueFromPipelineByPropertyName = $true)] [System.Management.Automation.SwitchParameter] $Force ) $osPlatform = Get-OSPlatform if ($osPlatform -eq 'MacOS') { throw 'The Install-GuestConfigurationPackage cmdlet is not supported on MacOS' } $verbose = $VerbosePreference -ne 'SilentlyContinue' -or ($PSBoundParameters.ContainsKey('Verbose') -and ($PSBoundParameters['Verbose'] -eq $true)) $systemPSModulePath = [Environment]::GetEnvironmentVariable('PSModulePath', 'Process') $guestConfigurationPolicyPath = Get-GuestConfigPolicyPath try { # Unzip Guest Configuration binaries if missing Install-GuestConfigurationAgent -verbose:$verbose # Resolve the zip (to temp folder if URI) if (($Path -as [uri]).Scheme -match '^http') { # Get the package from URI to a temp folder $PackageZipPath = (Get-GuestConfigurationPackageFromUri -Uri $Path -Verbose:$verbose).ToString() } elseif ((Test-Path -PathType 'Leaf' -Path $Path) -and $Path -match '\.zip$') { $PackageZipPath = (Resolve-Path -Path $Path).ToString() } else { # The $Path parameter is not a valid path or URL throw "'$Path' is not a valid path to the package. Please provide the path to the Zip or the URL to download the package from." } Write-Debug -Message "Getting package name from '$PackageZipPath'." $packageName = Get-GuestConfigurationPackageNameFromZip -Path $PackageZipPath $packageZipMetadata = Get-GuestConfigurationPackageMetadataFromZip -Path $PackageZipPath -Verbose:$verbose $installedPackagePath = Join-Path -Path $guestConfigurationPolicyPath -ChildPath $packageName $isPackageAlreadyInstalled = $false if (Test-Path -Path $installedPackagePath) { Write-Debug -Message "The Package '$PackageName' exists at '$installedPackagePath'. Checking version..." $installedPackageMetadata = Get-GuestConfigurationPackageMetaConfig -Path $installedPackagePath -Verbose:$verbose # None of the packages are versioned or the versions match, we're good if (-not ($installedPackageMetadata.ContainsKey('Version') -or $packageZipMetadata.Contains('Version')) -or ($installedPackageMetadata.ContainsKey('Version') -ne $packageZipMetadata.Contains('Version')) -or # to avoid next statement $installedPackageMetadata.Version -eq $packageZipMetadata.Version) { $isPackageAlreadyInstalled = $true Write-Debug -Message ("Package '{0}{1}' is installed." -f $PackageName,($packageZipMetadata.Contains('Version') ? "_$($packageZipMetadata['Version'])" : '')) } else { Write-Verbose -Message "Package '$packageName' was found at version '$($installedPackageMetadata.Version)' but we're expecting '$($packageZipMetadata.Version)'." } } if ($PSBoundParameters.ContainsKey('Force') -and $PSBoundParameters['Force']) { $withForce = $true } else { $withForce = $false } if ((-not $isPackageAlreadyInstalled) -or $withForce) { Write-Debug -Message "Removing existing package at '$installedPackagePath'." Remove-Item -Path $installedPackagePath -Recurse -Force -ErrorAction SilentlyContinue $null = New-Item -ItemType Directory -Force -Path $installedPackagePath # Unzip policy package Write-Verbose -Message "Unzipping the Guest Configuration Package to '$installedPackagePath'." Expand-Archive -LiteralPath $PackageZipPath -DestinationPath $installedPackagePath -ErrorAction Stop -Force } else { Write-Verbose -Message "Package is already installed at '$installedPackagePath', skipping install." } # Clear Inspec profiles Remove-Item -Path (Get-InspecProfilePath) -Recurse -Force -ErrorAction SilentlyContinue } finally { $env:PSModulePath = $systemPSModulePath # If we downloaded the Zip file from URI to temp folder, do cleanup if (($Path -as [uri]).Scheme -match '^http') { Write-Debug -Message "Removing the Package zip at '$PackageZipPath' that was downloaded from URI." Remove-Item -Force -ErrorAction SilentlyContinue -Path $PackageZipPath } } return $installedPackagePath } #EndRegion './Public/Install-GuestConfigurationPackage.ps1' 134 #Region './Public/New-GuestConfigurationPackage.ps1' 0 <# .SYNOPSIS Creates a package to run code on machines for use through Azure Guest Configuration. .PARAMETER Name The name of the Guest Configuration package. .PARAMETER Version The semantic version of the Guest Configuration package. This is a tag for you to keep track of your pacakges; it is not currently used by Guest Configuration or Azure Policy. .PARAMETER Configuration The path to the compiled DSC configuration file (.mof) to base the package on. .PARAMETER Path The path to a folder to output the package under. By default the package will be created under the current directory. .PARAMETER ChefInspecProfilePath The path to a folder containing Chef InSpec profiles to include with the package. The compiled DSC configuration (.mof) provided should 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. .PARAMETER Type Sets a tag in the metaconfig data of the package specifying whether or not this package can support Set functionality or not. This tag is currently used only for verfication by this module and does not affect the functionality of the package. AuditAndSet indicates that the package may be used for setting the state of the machine. Audit indicates that the package will not set the state of the machine and may only monitor settings. By default this tag is set to Audit. .PARAMETER Force If present, this function will overwrite any existing package files. .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. #> 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.String] $Configuration, [Parameter(Position = 2, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [System.String] $Version = '0.0.0', [Parameter()] [System.String] $ChefInspecProfilePath, [Parameter()] [System.String] $FilesToInclude, [Parameter()] [ValidateNotNullOrEmpty()] [System.String] $Path = '.', [Parameter()] [ValidateSet('Audit', 'AuditAndSet')] [ValidateNotNullOrEmpty()] [String] $Type = 'Audit', [Parameter()] [Switch] $Force ) Write-Verbose -Message 'Starting New-GuestConfigurationPackage' #-----VALIDATION----- # 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-ResouceDependenciesFromMof -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 if (-not [string]::IsNullOrEmpty($FilesToInclude)) { if (-not (Test-Path -Path $FilesToInclude)) { throw "The item to include from the path '$FilesToInclude' 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 "An item already exists at the package path '$packageRootPath'. Please remove it or use the Force parameter." } } # Check destination $packageDestinationPath = "$packageRootPath.zip" if (Test-Path -Path $packageDestinationPath) { if (-not $Force) { throw "An item already exists at the package destination path '$packageDestinationPath'. Please remove it or use the Force parameter." } } #-----PACKAGE CREATION----- # Clear the root package folder if (Test-Path -Path $packageRootPath) { Write-Verbose -Message "Removing existing package at the path '$packageRootPath'..." $null = Remove-Item -Path $packageRootPath -Recurse -Force } $null = New-Item -Path $packageRootPath -ItemType 'Directory' -Force # Clear the package destination if (Test-Path -Path $packageDestinationPath) { Write-Verbose -Message "Removing existing package zip at the path '$packageDestinationPath'..." $null = Remove-Item -Path $packageDestinationPath -Recurse -Force } # Create the package structure $modulesFolderPath = Join-Path -Path $packageRootPath -ChildPath 'Modules' $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 } $metaconfigJson = $metaconfig | ConvertTo-Json $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 $null = Copy-Item -Path $Configuration -Destination $mofFilePath # Edit the native Chef InSpec resource parameters in the mof if needed if ($usingInSpecResource) { Edit-ChefInSpecMofContent -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' $null = New-Item -Path $nativeResourcesFolder -ItemType 'Directory' $inSpecResourceFolder = Join-Path -Path $nativeResourcesFolder -ChildPath 'MSFT_ChefInSpecResource' $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' $null = Copy-Item -Path $installInSpecScriptSourcePath -Destination $modulesFolderPath $inSpecResourceLibrarySourcePath = Join-Path -Path $inSpecResourceSourcePath -ChildPath 'libMSFT_ChefInSpecResource.so' $null = Copy-Item -Path $inSpecResourceLibrarySourcePath -Destination $inSpecResourceFolder $inSpecResourceSchemaMofSourcePath = Join-Path -Path $inSpecResourceSourcePath -ChildPath 'MSFT_ChefInSpecResource.schema.mof' $null = Copy-Item -Path $inSpecResourceSchemaMofSourcePath -Destination $inSpecResourceFolder foreach ($inSpecProfileSourcePath in $inSpecProfileSourcePaths) { $null = Copy-Item -Path $inSpecProfileSourcePath -Destination $modulesFolderPath -Container -Recurse } } # Copy extra files if (-not [string]::IsNullOrEmpty($FilesToInclude)) { if (Test-Path $FilesToInclude -PathType 'Leaf') { $null = Copy-Item -Path $FilesToInclude -Destination $modulesFolderPath } else { $null = Copy-Item -Path $FilesToInclude -Destination $modulesFolderPath -Container -Recurse } } # Zip the package $null = Compress-ArchiveWithPermissions -Path $packageRootPath -DestinationPath $packageDestinationPath return [PSCustomObject]@{ PSTypeName = 'GuestConfiguration.Package' Name = $Name Path = $packageDestinationPath } } #EndRegion './Public/New-GuestConfigurationPackage.ps1' 351 #Region './Public/New-GuestConfigurationPolicy.ps1' 0 <# .SYNOPSIS Creates Audit, DeployIfNotExists and Initiative policy definitions on specified Destination Path. .Parameter ContentUri Public http uri of Guest Configuration content package. .Parameter DisplayName Policy display name. .Parameter Description Policy description. .Parameter Parameter Policy parameters. .Parameter Version Policy version. .Parameter Path Destination path. .Parameter Platform Target platform (Windows/Linux) for Guest Configuration policy and content package. Windows is the default platform. .Parameter Mode Defines whether or not the policy is Audit or Deploy. Acceptable values: Audit, ApplyAndAutoCorrect, or ApplyAndMonitor. Audit is the default mode. .Parameter Tag The name and value of a tag used in Azure. .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.' ` -Version 1.0.0.0 -Path ./git/custom_policy -Tag @{Owner = 'WebTeam'} $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 (optional) ResourceType = "Service" # dsc configuration resource type (mandatory) ResourceId = 'windowsService' # dsc configuration resource property name (mandatory) ResourcePropertyName = "Name" # dsc 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.' ` -Version 1.0.0.0 -Path ./policyDefinitions ` -Parameter $PolicyParameterInfo .OUTPUTS Return name and path of the Guest Configuration policy definitions. #> function New-GuestConfigurationPolicy { [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [System.Uri] $ContentUri, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $DisplayName, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Description, [Parameter()] [System.Collections.Hashtable[]] $Parameter, [Parameter()] [ValidateNotNullOrEmpty()] [System.Version] $Version = '1.0.0', [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Path, [Parameter()] [ValidateSet('Windows', 'Linux')] [System.String] $Platform = 'Windows', [Parameter()] [AssignmentType] $Mode = 'Audit', [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $PolicyId, [Parameter()] [System.Collections.Hashtable[]] $Tag ) # This value must be static for AINE policies due to service configuration $Category = 'Guest Configuration' try { $verbose = ($PSBoundParameters.ContainsKey("Verbose") -and ($PSBoundParameters["Verbose"] -eq $true)) $policyDefinitionsPath = $Path $unzippedPkgPath = Join-Path -Path $policyDefinitionsPath -ChildPath 'temp' $tempContentPackageFilePath = Join-Path -Path $policyDefinitionsPath -ChildPath 'temp.zip' # Update parameter info $ParameterInfo = Update-PolicyParameter -Parameter $Parameter $null = New-Item -ItemType Directory -Force -Path $policyDefinitionsPath # Check if ContentUri is a valid web URI if (-not ($null -ne $ContentUri.AbsoluteURI -and $ContentUri.Scheme -match '[http|https]')) { throw "Invalid ContentUri : $ContentUri. Please specify a valid http URI in -ContentUri parameter." } # Generate checksum hash for policy content. Invoke-WebRequest -Uri $ContentUri -OutFile $tempContentPackageFilePath $tempContentPackageFilePath = Resolve-Path $tempContentPackageFilePath $contentHash = (Get-FileHash $tempContentPackageFilePath -Algorithm SHA256).Hash Write-Verbose "SHA256 Hash for content '$ContentUri' : $contentHash." # Get the policy name from policy content. Remove-Item $unzippedPkgPath -Recurse -Force -ErrorAction SilentlyContinue New-Item -ItemType Directory -Force -Path $unzippedPkgPath | Out-Null $unzippedPkgPath = Resolve-Path $unzippedPkgPath Add-Type -AssemblyName System.IO.Compression.FileSystem [System.IO.Compression.ZipFile]::ExtractToDirectory($tempContentPackageFilePath, $unzippedPkgPath) $dscDocument = Get-ChildItem -Path $unzippedPkgPath -Filter *.mof -Exclude '*.schema.mof' -Depth 1 if (-not $dscDocument) { throw "Invalid policy package, failed to find dsc document in policy package." } $policyName = [System.IO.Path]::GetFileNameWithoutExtension($dscDocument) $packageIsSigned = (($null -ne (Get-ChildItem -Path $unzippedPkgPath -Filter *.cat)) -or (($null -ne (Get-ChildItem -Path $unzippedPkgPath -Filter *.asc)) -and ($null -ne (Get-ChildItem -Path $unzippedPkgPath -Filter *.sha256sums)))) # Determine if policy is AINE or DINE if ($Mode -eq "Audit") { $FileName = 'AuditIfNotExists.json' } else { $FileName = 'DeployIfNotExists.json' } $PolicyInfo = @{ FileName = $FileName DisplayName = $DisplayName Description = $Description Platform = $Platform ConfigurationName = $policyName ConfigurationVersion = $Version ContentUri = $ContentUri ContentHash = $contentHash AssignmentType = $Mode ReferenceId = "Deploy_$policyName" ParameterInfo = $ParameterInfo UseCertificateValidation = $packageIsSigned Category = $Category Guid = $PolicyId Tag = $Tag } $null = New-CustomGuestConfigPolicy -PolicyFolderPath $policyDefinitionsPath -PolicyInfo $PolicyInfo -Verbose:$verbose [pscustomobject]@{ PSTypeName = 'GuestConfiguration.Policy' Name = $policyName Path = $Path } } finally { # Remove staging content package. Remove-Item -Path $tempContentPackageFilePath -Force -ErrorAction SilentlyContinue Remove-Item -Path $unzippedPkgPath -Recurse -Force -ErrorAction SilentlyContinue } } #EndRegion './Public/New-GuestConfigurationPolicy.ps1' 206 #Region './Public/Protect-GuestConfigurationPackage.ps1' 0 <# .SYNOPSIS Signs a Guest Configuration policy package using certificate on Windows and Gpg keys on Linux. .Parameter Path Full path of the Guest Configuration package. .Parameter Certificate 'Code Signing' certificate to sign the package. This is only supported on Windows. .Parameter PrivateGpgKeyPath Private Gpg key path. This is only supported on Linux. .Parameter PublicGpgKeyPath Public Gpg key path. This is only supported on Linux. .Example $Cert = Get-ChildItem -Path Cert:/CurrentUser/AuthRoot -Recurse | Where-Object {($_.Thumbprint -eq "0563b8630d62d75abbc8ab1e4bdfb5a899b65d43") } Protect-GuestConfigurationPackage -Path ./custom_policy/WindowsTLS.zip -Certificate $Cert .OUTPUTS Return name and path of the signed Guest Configuration Policy package. #> 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 ) $Path = Resolve-Path $Path if (-not (Test-Path $Path -PathType Leaf)) { throw 'Invalid Guest Configuration package path.' } try { $packageFileName = [System.IO.Path]::GetFileNameWithoutExtension($Path) $signedPackageFilePath = Join-Path (Get-ChildItem $Path).Directory "$($packageFileName)_signed.zip" $tempDir = Join-Path -Path (Get-ChildItem $Path).Directory -ChildPath 'temp' Remove-Item $signedPackageFilePath -Force -ErrorAction SilentlyContinue $null = New-Item -ItemType Directory -Force -Path $tempDir # Unzip policy package. Add-Type -AssemblyName System.IO.Compression.FileSystem [System.IO.Compression.ZipFile]::ExtractToDirectory($Path, $tempDir) # Get policy name $dscDocument = Get-ChildItem -Path $tempDir -Filter *.mof if (-not $dscDocument) { throw "Invalid policy package, failed to find dsc document in policy package." } $policyName = [System.IO.Path]::GetFileNameWithoutExtension($dscDocument) $osPlatform = Get-OSPlatform if ($PSCmdlet.ParameterSetName -eq "Certificate") { if ($osPlatform -eq "Linux") { throw 'Certificate signing not supported on Linux.' } # Create catalog file $catalogFilePath = Join-Path -Path $tempDir -ChildPath "$policyName.cat" Remove-Item $catalogFilePath -Force -ErrorAction SilentlyContinue Write-Verbose "Creating catalog file : $catalogFilePath." New-FileCatalog -Path $tempDir -CatalogVersion 2.0 -CatalogFilePath $catalogFilePath | Out-Null # Sign catalog file Write-Verbose "Signing catalog file : $catalogFilePath." $CodeSignOutput = Set-AuthenticodeSignature -Certificate $Certificate -FilePath $catalogFilePath $Signature = Get-AuthenticodeSignature $catalogFilePath if ($null -ne $Signature.SignerCertificate) { if ($Signature.SignerCertificate.Thumbprint -ne $Certificate.Thumbprint) { throw $CodeSignOutput.StatusMessage } } else { throw $CodeSignOutput.StatusMessage } } else { if ($osPlatform -eq "Windows") { throw 'Gpg signing not supported on Windows.' } $PrivateGpgKeyPath = Resolve-Path $PrivateGpgKeyPath $PublicGpgKeyPath = Resolve-Path $PublicGpgKeyPath $ascFilePath = Join-Path $tempDir "$policyName.asc" $hashFilePath = Join-Path $tempDir "$policyName.sha256sums" Remove-Item $ascFilePath -Force -ErrorAction SilentlyContinue Remove-Item $hashFilePath -Force -ErrorAction SilentlyContinue Write-Verbose "Creating file hash : $hashFilePath." Push-Location -Path $tempDir bash -c "find ./ -type f -print0 | xargs -0 sha256sum | grep -v sha256sums > $hashFilePath" Pop-Location Write-Verbose "Signing file hash : $hashFilePath." gpg --import $PrivateGpgKeyPath gpg --no-default-keyring --keyring $PublicGpgKeyPath --output $ascFilePath --armor --detach-sign $hashFilePath } # Zip the signed Guest Configuration package Write-Verbose "Creating signed Guest Configuration package : '$signedPackageFilePath'." [System.IO.Compression.ZipFile]::CreateFromDirectory($tempDir, $signedPackageFilePath) $result = [pscustomobject]@{ Name = $policyName Path = $signedPackageFilePath } return $result } finally { Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue } } #EndRegion './Public/Protect-GuestConfigurationPackage.ps1' 151 #Region './Public/Publish-GuestConfigurationPackage.ps1' 0 <# .SYNOPSIS Publish a Guest Configuration policy package to Azure blob storage. The goal is to simplify the number of steps by scoping to a specific task. Generates a SAS token with a 3-year lifespan, to mitigate the risk of a malicious person discovering the published content. Requires a resource group, storage account, and container to be pre-staged. For details on how to pre-stage these things see the documentation for the Az Storage cmdlets. https://docs.microsoft.com/en-us/azure/storage/blobs/storage-quickstart-blobs-powershell. .Parameter Path Location of the .zip file containing the Guest Configuration artifacts .Parameter ResourceGroupName The Azure resource group for the storage account .Parameter StorageAccountName The name of the storage account for where the package will be published Storage account names must be globally unique .Parameter StorageContainerName Name of the storage container in Azure Storage account (default: "guestconfiguration") .Example Publish-GuestConfigurationPackage -Path ./package.zip -ResourceGroupName 'resourcegroup' -StorageAccountName 'sa12345' .OUTPUTS Return a publicly accessible URI containing a SAS token with a 3-year expiration. #> function Publish-GuestConfigurationPackage { [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [System.String] $Path, [Parameter(Position = 1, Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $ResourceGroupName, [Parameter(Position = 2, Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $StorageAccountName, [Parameter()] [System.String] $StorageContainerName = 'guestconfiguration', [Parameter()] [System.Management.Automation.SwitchParameter] $Force ) # Get Storage Context $Context = Get-AzStorageAccount -ResourceGroupName $ResourceGroupName -Name $StorageAccountName | ForEach-Object { $_.Context } # Blob name from file name $BlobName = (Get-Item -Path $Path -ErrorAction Stop).Name $setAzStorageBlobContentParams = @{ Context = $Context Container = $StorageContainerName Blob = $BlobName File = $Path } if ($true -eq $Force) { $setAzStorageBlobContentParams.Add('Force', $true) } # Upload file $null = Set-AzStorageBlobContent @setAzStorageBlobContentParams # Get url with SAS token # THREE YEAR EXPIRATION $StartTime = Get-Date $newAzStorageBlobSASTokenParams = @{ Context = $Context Container = $StorageContainerName Blob = $BlobName StartTime = $StartTime ExpiryTime = $StartTime.AddYears('3') Permission = 'rl' FullUri = $true } $SAS = New-AzStorageBlobSASToken @newAzStorageBlobSASTokenParams # Output return [PSCustomObject]@{ ContentUri = $SAS } } #EndRegion './Public/Publish-GuestConfigurationPackage.ps1' 107 #Region './Public/Publish-GuestConfigurationPolicy.ps1' 0 <# .SYNOPSIS Publishes the Guest Configuration policy in Azure Policy Center. .Parameter Path Guest Configuration policy path. .Example Publish-GuestConfigurationPolicy -Path ./git/custom_policy #> function Publish-GuestConfigurationPolicy { [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [System.String] $Path, [Parameter()] [System.String] $ManagementGroupName ) $rmContext = Get-AzContext Write-Verbose -Message "Publishing Guest Configuration policy using '$($rmContext.Name)' AzContext." # Publish policies $currentFiles = @(Get-ChildItem $Path | Where-Object -FilterScript { $_.name -like "DeployIfNotExists.json" -or $_.name -like "AuditIfNotExists.json" }) if ($currentFiles.Count -eq 0) { throw "No valid AuditIfNotExists.json or DeployIfNotExists.json files found at $Path" } elseif ($currentFiles.Count -gt 1) { throw "More than one valid json found at $Path" } $policyFile = $currentFiles[0] $jsonDefinition = Get-Content -Path $policyFile | ConvertFrom-Json | ForEach-Object { $_ } $definitionContent = $jsonDefinition.Properties $newAzureRmPolicyDefinitionParameters = @{ Name = $jsonDefinition.name DisplayName = $($definitionContent.DisplayName | ConvertTo-Json -Depth 20).replace('"', '') Description = $($definitionContent.Description | ConvertTo-Json -Depth 20).replace('"', '') Policy = $($definitionContent.policyRule | ConvertTo-Json -Depth 20) Metadata = $($definitionContent.Metadata | ConvertTo-Json -Depth 20) ApiVersion = '2018-05-01' Verbose = $true } if ($definitionContent.PSObject.Properties.Name -contains 'parameters') { $newAzureRmPolicyDefinitionParameters['Parameter'] = ConvertTo-Json -InputObject $definitionContent.parameters -Depth 15 } if ($ManagementGroupName) { $newAzureRmPolicyDefinitionParameters['ManagementGroupName'] = $ManagementGroupName } Write-Verbose -Message "Publishing '$($jsonDefinition.properties.displayName)' ..." New-AzPolicyDefinition @newAzureRmPolicyDefinitionParameters } #EndRegion './Public/Publish-GuestConfigurationPolicy.ps1' 71 #Region './Public/Start-GuestConfigurationPackageRemediation.ps1' 0 <# .SYNOPSIS Starting to remediate a Guest Configuration policy package. .Parameter Path Relative/Absolute local path of the zipped Guest Configuration package. .Parameter Parameter Policy parameters. .Parameter Force Allows cmdlet to make changes on machine for remediation that cannot otherwise be changed. .Example Start-GuestConfigurationPackage -Path ./custom_policy/WindowsTLS.zip -Force $Parameter = @( @{ ResourceType = "MyFile" # dsc configuration resource type (mandatory) ResourceId = 'hi' # dsc configuration resource property id (mandatory) ResourcePropertyName = "Ensure" # dsc configuration resource property name (mandatory) ResourcePropertyValue = 'Present' # dsc configuration resource property value (mandatory) }) Start-GuestConfigurationPackage -Path ./custom_policy/AuditWindowsService.zip -Parameter $Parameter -Force .OUTPUTS None. #> function Start-GuestConfigurationPackageRemediation { [CmdletBinding()] [OutputType()] param ( [Parameter(Position = 0, Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [string] $Path, [Parameter()] [Switch] $Force, [Parameter()] [Hashtable[]] $Parameter = @() ) $osPlatform = Get-OSPlatform if ($osPlatform -eq 'MacOS') { throw 'The Install-GuestConfigurationPackage cmdlet is not supported on MacOS' } $verbose = ($PSBoundParameters.ContainsKey('Verbose') -and ($PSBoundParameters['Verbose'] -eq $true)) $systemPSModulePath = [Environment]::GetEnvironmentVariable('PSModulePath', 'Process') if ($PSBoundParameters.ContainsKey('Force') -and $Force) { $withForce = $true } else { $withForce = $false } try { # Install the package $packagePath = Install-GuestConfigurationPackage -Path $Path -Force:$withForce -ErrorAction 'Stop' # The leaf part of the Path returned by Install-GCPackage will always be the BaseName of the MOF. $packageName = Get-GuestConfigurationPackageName -Path $packagePath # Confirm mof exists $packageMof = Join-Path -Path $packagePath -ChildPath "$packageName.mof" $dscDocument = Get-Item -Path $packageMof -ErrorAction 'SilentlyContinue' if (-not $dscDocument) { throw "Invalid Guest Configuration package, failed to find dsc document at $packageMof path." } # Throw if package is not set to AuditAndSet. If metaconfig is not found, assume Audit. $metaConfig = Get-GuestConfigurationPackageMetaConfig -Path $packagePath if ($metaConfig.Type -ne "AuditAndSet") { throw "Cannot run Start-GuestConfigurationPackage on a package that is not set to AuditAndSet. Current metaconfig contents: $metaconfig" } # Update mof values if ($Parameter.Count -gt 0) { Write-Debug -Message "Updating MOF with $($Parameter.Count) parameters." Update-MofDocumentParameters -Path $dscDocument.FullName -Parameter $Parameter } Write-Verbose -Message "Publishing policy package '$packageName' from '$packagePath'." Publish-DscConfiguration -ConfigurationName $packageName -Path $packagePath -Verbose:$verbose # Set LCM settings to force load powershell module. $metaConfigPath = Join-Path -Path $packagePath -ChildPath "$packageName.metaconfig.json" Write-Debug -Message "Setting 'LCM' Debug mode to force module import." Update-GuestConfigurationPackageMetaconfig -metaConfigPath $metaConfigPath -Key 'debugMode' -Value 'ForceModuleImport' Write-Debug -Message "Setting 'LCM' configuration mode to ApplyAndMonitor." Update-GuestConfigurationPackageMetaconfig -metaConfigPath $metaConfigPath -Key 'configurationMode' -Value 'ApplyAndMonitor' Set-DscLocalConfigurationManager -ConfigurationName $packageName -Path $packagePath -Verbose:$verbose # Run Deploy/Remediation Start-DscConfiguration -ConfigurationName $packageName -Verbose:$verbose } finally { $env:PSModulePath = $systemPSModulePath } } #EndRegion './Public/Start-GuestConfigurationPackageRemediation.ps1' 119 #Region './Public/Test-GuestConfigurationPackage.ps1' 0 <# .SYNOPSIS Tests a Guest Configuration policy package. .Parameter Path Full path of the zipped Guest Configuration package. .Parameter Parameter Policy parameters. .Example Test-GuestConfigurationPackage -Path ./custom_policy/WindowsTLS.zip $Parameter = @( @{ ResourceType = "Service" # dsc configuration resource type (mandatory) ResourceId = 'windowsService' # dsc configuration resource property id (mandatory) ResourcePropertyName = "Name" # dsc configuration resource property name (mandatory) ResourcePropertyValue = 'winrm' # dsc configuration resource property value (mandatory) }) Test-GuestConfigurationPackage -Path ./custom_policy/AuditWindowsService.zip -Parameter $Parameter .OUTPUTS Returns compliance details. #> function Test-GuestConfigurationPackage { [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [string] $Path, [Parameter()] [Hashtable[]] $Parameter = @(), [Parameter()] [Switch] $Force ) if ($IsMacOS) { throw 'The Test-GuestConfigurationPackage cmdlet is not supported on MacOS' } # Determine if verbose is enabled to pass down to other functions $verbose = ($PSBoundParameters.ContainsKey("Verbose") -and ($PSBoundParameters["Verbose"] -eq $true)) $systemPSModulePath = [Environment]::GetEnvironmentVariable("PSModulePath", "Process") $gcBinPath = Get-GuestConfigBinaryPath $guestConfigurationPolicyPath = Get-GuestConfigPolicyPath if ($PSBoundParameters.ContainsKey('Force') -and $PSBoundParameters['Force']) { $withForce = $true } else { $withForce = $false } try { # Get the installed policy path, and install if missing $packagePath = Install-GuestConfigurationPackage -Path $Path -Verbose:$verbose -Force:$withForce $packageName = Get-GuestConfigurationPackageName -Path $packagePath Write-Debug -Message "PackageName: '$packageName'." # Confirm mof exists $packageMof = Join-Path -Path $packagePath -ChildPath "$packageName.mof" $dscDocument = Get-Item -Path $packageMof -ErrorAction 'SilentlyContinue' if (-not $dscDocument) { throw "Invalid Guest Configuration package, failed to find dsc document at '$packageMof' path." } # update configuration parameters if ($Parameter.Count -gt 0) { Write-Debug -Message "Updating MOF with $($Parameter.Count) parameters." Update-MofDocumentParameters -Path $dscDocument.FullName -Parameter $Parameter } Write-Verbose -Message "Publishing policy package '$packageName' from '$packagePath'." Publish-DscConfiguration -ConfigurationName $packageName -Path $packagePath -Verbose:$verbose # Set LCM settings to force load powershell module. Write-Debug -Message "Setting 'LCM' Debug mode to force module import." $metaConfigPath = Join-Path -Path $packagePath -ChildPath "$packageName.metaconfig.json" Update-GuestConfigurationPackageMetaconfig -metaConfigPath $metaConfigPath -Key 'debugMode' -Value 'ForceModuleImport' Set-DscLocalConfigurationManager -ConfigurationName $packageName -Path $packagePath -Verbose:$verbose $inspecProfilePath = Get-InspecProfilePath Write-Debug -Message "Clearing Inspec profiles at '$inspecProfilePath'." Remove-Item -Path $inspecProfilePath -Recurse -Force -ErrorAction SilentlyContinue Write-Verbose -Message "Getting Configuration resources status." $getResult = @() $getResult = $getResult + (Get-DscConfiguration -ConfigurationName $packageName -Verbose:$verbose) return $getResult } finally { $env:PSModulePath = $systemPSModulePath } } #EndRegion './Public/Test-GuestConfigurationPackage.ps1' 113 # SIG # Begin signature block # MIIjgwYJKoZIhvcNAQcCoIIjdDCCI3ACAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCa1BNHJ7qgwBT/ # EbnhMwd9EBtPjhB8xuaPQPnkGVwp1aCCDYEwggX/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/BvW1taslScxMNelDNMYIVWDCCFVQCAQEwgZUwfjELMAkG # A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx # HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z # b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAlKLM6r4lfM52wAAAAACUjAN # BglghkgBZQMEAgEFAKCBrjAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor # BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgCZUqabw4 # cqYg2qZrZDNqxi0zNPhVkwyVAl87SuZ0mKEwQgYKKwYBBAGCNwIBDDE0MDKgFIAS # AE0AaQBjAHIAbwBzAG8AZgB0oRqAGGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbTAN # BgkqhkiG9w0BAQEFAASCAQBVXuh6DkH+c9G+pw/gujMoZGNglpjZvycYI2SaXXTq # Y+ArDO/Ji+/FRw+gwvJiP/loZF2fiZwUn3HoFDT3VBrsTVH0w5ehy1NbNcUmwDeV # pu4nyzHZ0oaPyD4a5O6bFqh3GBTcs7nHaOGfmH0JAV/Awgk+YyriuFSmjYCdU/di # mCR4OEmy2iKghIcOGlEYokCtxZL8BsKaqq4NUVm1S2B5cVDboD8tQRDIoIeR/1SP # pu2pb6K9Xs+1oPLpohSAv7BmvyAsZp6y4kgbfNfXDsA5GdbQPyXFQwDjLR5Uoy5K # CEsPbowtETpGeRmJgAYZBn794m4DO63YDv1xgBShSbJxoYIS4jCCEt4GCisGAQQB # gjcDAwExghLOMIISygYJKoZIhvcNAQcCoIISuzCCErcCAQMxDzANBglghkgBZQME # AgEFADCCAVEGCyqGSIb3DQEJEAEEoIIBQASCATwwggE4AgEBBgorBgEEAYRZCgMB # MDEwDQYJYIZIAWUDBAIBBQAEIBTjDtUHbY2yeqyIRHvgec0BHuTy4KG/VRPSbrJg # ZkwrAgZhktZXsoAYEzIwMjExMTI0MjMyNDMxLjgwNVowBIACAfSggdCkgc0wgcox # CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt # b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJTAjBgNVBAsTHE1p # Y3Jvc29mdCBBbWVyaWNhIE9wZXJhdGlvbnMxJjAkBgNVBAsTHVRoYWxlcyBUU1Mg # RVNOOkVBQ0UtRTMxNi1DOTFEMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFt # cCBTZXJ2aWNloIIOOTCCBPEwggPZoAMCAQICEzMAAAFMxUzB0NtvP7IAAAAAAUww # DQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0 # b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3Jh # dGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwHhcN # MjAxMTEyMTgyNjAwWhcNMjIwMjExMTgyNjAwWjCByjELMAkGA1UEBhMCVVMxEzAR # BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p # Y3Jvc29mdCBDb3Jwb3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2Eg # T3BlcmF0aW9uczEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046RUFDRS1FMzE2LUM5 # MUQxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggEiMA0G # CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDKYWBRw6zHue+jVeyBCg/45N+8T4mk # 43ntsyt1z/qlCaQVTGNiAsWkUYctQp8n/+b1kaZ99wZPcWxnoJ6W5KC/PNGzaUme # rlnKc0oBQTnZjVK0wbfekVl2j2O5LVDAWRFr4kn98gldiF4FmAEBbmfbLEPWII6a # Nab1K7WqFMAI4mjON+lAlPX1tQ/pHBB9OZuIbnFmxPCVvjvW925XrYr+/J/nwuqC # pOmkkEURS+DiYqL0vom9e+RuqUn/cA0ZPV95DuutTrQnKx2QH8HtjB1wz+HmXxkZ # LAPyL76yxTXGoyOyLek8fqJw8keYoEYvpAiaExtGFBgtVDIwitOVrQ67AgMBAAGj # ggEbMIIBFzAdBgNVHQ4EFgQUAZYepwQKXucnlUIBgPQQR95m+nwwHwYDVR0jBBgw # FoAU1WM6XIoxkPNDe3xGG8UzaFqFbVUwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDov # L2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljVGltU3RhUENB # XzIwMTAtMDctMDEuY3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0 # cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNUaW1TdGFQQ0FfMjAx # MC0wNy0wMS5jcnQwDAYDVR0TAQH/BAIwADATBgNVHSUEDDAKBggrBgEFBQcDCDAN # BgkqhkiG9w0BAQsFAAOCAQEATwTksPfWQ66ogGKvd+tmdx2IQYaEl7zHiAhvccO6 # 5afIQLZokhzyAHDO+MZH2GZ3QX9WUObp1OWJlfvzxv0LuzV/GSoJHLDVvFDwJ1W0 # 6UfrzZn//5F3YgyT92/FO5zM2dOaXkSjFeL1DhGA+vsMPBzUkgRI0VX2hEgS2d6K # Yz6Mc2smqKfll1OWVrZaJpd6C657ptbInE1asN9JjNo2P8CSR/2yuG00c87+7e59 # fIAf/lwv2Ef49vrSLp7Y9MS9EFBRtF7gQC/usy0grSUd+qtIT/++2bJNLcS/eZjX # K0X0UCcuMU+ZZBiGV2wMhEIOdQRuWqJlTv9ftOb67c/KazCCBnEwggRZoAMCAQIC # CmEJgSoAAAAAAAIwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRp # ZmljYXRlIEF1dGhvcml0eSAyMDEwMB4XDTEwMDcwMTIxMzY1NVoXDTI1MDcwMTIx # NDY1NVowfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNV # BAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQG # A1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwggEiMA0GCSqGSIb3 # DQEBAQUAA4IBDwAwggEKAoIBAQCpHQ28dxGKOiDs/BOX9fp/aZRrdFQQ1aUKAIKF # ++18aEssX8XD5WHCdrc+Zitb8BVTJwQxH0EbGpUdzgkTjnxhMFmxMEQP8WCIhFRD # DNdNuDgIs0Ldk6zWczBXJoKjRQ3Q6vVHgc2/JGAyWGBG8lhHhjKEHnRhZ5FfgVSx # z5NMksHEpl3RYRNuKMYa+YaAu99h/EbBJx0kZxJyGiGKr0tkiVBisV39dx898Fd1 # rL2KQk1AUdEPnAY+Z3/1ZsADlkR+79BL/W7lmsqxqPJ6Kgox8NpOBpG2iAg16Hgc # sOmZzTznL0S6p/TcZL2kAcEgCZN4zfy8wMlEXV4WnAEFTyJNAgMBAAGjggHmMIIB # 4jAQBgkrBgEEAYI3FQEEAwIBADAdBgNVHQ4EFgQU1WM6XIoxkPNDe3xGG8UzaFqF # bVUwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud # EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/oolxiaNE9lJBb186aGMQwVgYD # VR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwv # cHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3JsMFoGCCsGAQUFBwEB # BE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9j # ZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcnQwgaAGA1UdIAEB/wSBlTCB # kjCBjwYJKwYBBAGCNy4DMIGBMD0GCCsGAQUFBwIBFjFodHRwOi8vd3d3Lm1pY3Jv # c29mdC5jb20vUEtJL2RvY3MvQ1BTL2RlZmF1bHQuaHRtMEAGCCsGAQUFBwICMDQe # MiAdAEwAZQBnAGEAbABfAFAAbwBsAGkAYwB5AF8AUwB0AGEAdABlAG0AZQBuAHQA # LiAdMA0GCSqGSIb3DQEBCwUAA4ICAQAH5ohRDeLG4Jg/gXEDPZ2joSFvs+umzPUx # vs8F4qn++ldtGTCzwsVmyWrf9efweL3HqJ4l4/m87WtUVwgrUYJEEvu5U4zM9GAS # inbMQEBBm9xcF/9c+V4XNZgkVkt070IQyK+/f8Z/8jd9Wj8c8pl5SpFSAK84Dxf1 # L3mBZdmptWvkx872ynoAb0swRCQiPM/tA6WWj1kpvLb9BOFwnzJKJ/1Vry/+tuWO # M7tiX5rbV0Dp8c6ZZpCM/2pif93FSguRJuI57BlKcWOdeyFtw5yjojz6f32WapB4 # pm3S4Zz5Hfw42JT0xqUKloakvZ4argRCg7i1gJsiOCC1JeVk7Pf0v35jWSUPei45 # V3aicaoGig+JFrphpxHLmtgOR5qAxdDNp9DvfYPw4TtxCd9ddJgiCGHasFAeb73x # 4QDf5zEHpJM692VHeOj4qEir995yfmFrb3epgcunCaw5u+zGy9iCtHLNHfS4hQEe # gPsbiSpUObJb2sgNVZl6h3M7COaYLeqN4DMuEin1wC9UJyH3yKxO2ii4sanblrKn # QqLJzxlBTeCG+SqaoxFmMNO7dDJL32N79ZmKLxvHIa9Zta7cRDyXUHHXodLFVeNp # 3lfB0d4wwP3M5k37Db9dT+mdHhk4L7zPWAUu7w2gUDXa7wknHNWzfjUeCLraNtvT # X4/edIhJEqGCAsswggI0AgEBMIH4oYHQpIHNMIHKMQswCQYDVQQGEwJVUzETMBEG # A1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWlj # cm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBP # cGVyYXRpb25zMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjpFQUNFLUUzMTYtQzkx # RDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcG # BSsOAwIaAxUAPZlbTgkoE2J2HRjNYygElxrg96CggYMwgYCkfjB8MQswCQYDVQQG # EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG # A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQg # VGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIFAOVJMfEwIhgPMjAy # MTExMjUwNTQ5MzdaGA8yMDIxMTEyNjA1NDkzN1owdDA6BgorBgEEAYRZCgQBMSww # KjAKAgUA5Ukx8QIBADAHAgEAAgIWETAHAgEAAgIRzTAKAgUA5UqDcQIBADA2Bgor # BgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAID # AYagMA0GCSqGSIb3DQEBBQUAA4GBAG5+k1FNHWmOceU5qbZtFyu7WlPZ/oE5f056 # Rv4SB1hjRSLMozmDEN6sxy5+1HD4e8UPDn9UWXQm+olVKyrXuBVmJ7ON+ywyknOu # oEaNxEGJWG4mCJjTUkXkKuf93i8Vy6GbwoX7JgNcZYPhfRSRcStbIeINkw7EthLN # P7/SpUxbMYIDDTCCAwkCAQEwgZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh # c2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD # b3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIw # MTACEzMAAAFMxUzB0NtvP7IAAAAAAUwwDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqG # SIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQg7hCYXiR+/9qb # WbAoE64sobj7y+TLuiOovp1vteTn5VkwgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHk # MIG9BCDbwqW7v1BLMtEQTCDh+k8LLYZTfdP7c9mvEGJnxK9OJTCBmDCBgKR+MHwx # CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt # b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1p # Y3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAABTMVMwdDbbz+yAAAAAAFM # MCIEICnZ+f7y8tLPD5WAdspt9qtH9PySTejJF+qHrPJ/2OpHMA0GCSqGSIb3DQEB # CwUABIIBAGnQIiHvm+Bt56/a8QmpeYx4thVZaBB+QznglAe921cB1eALcbLLt5hu # CxD6s0va033QCyyj0Pei2QopbNLA6lYCf+I0VbR4+qhO1JlT4Tv4Y0QNA1nC5qPE # Jtx+LijX73IXPB2KG6Y7kzhuEjKeMh21CujEPl94BuAS7tOtOdkyiF56wyxvX/Na # lviKiEbLFhQHxc7BzhC+CYANoCqlkjB6WegRPLuwsfOzLIa7SgkBaKp/zs1GeKmw # 8gxi3xvlwXedmiQGTyJT1oGkNBEKP7hiwMCy/BBHKkSG9s/4IVSoptACl+RRlVd8 # l4tuTlWSZWCqm6IBAt5Tmvdo/Q6JSJU= # SIG # End signature block |