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) -or
    ($PSVersionTable.PSVersion.Major -eq 7 -and ($PSVersionTable.PSVersion.PreReleaseLabel -and ($PSVersionTable.PSVersion.PreReleaseLabel -split '\.')[1] -lt 6))
    ))
{
    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' 29
#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-ArchiveByDirectory.ps1' 0
#using namespace System.IO.Compression.ZipFile

<#
    .SYNOPSIS
        Create an Zip file from a Directory, including hidden files and folders.

    .DESCRIPTION
        The Compress-Archive is not copying hidden files and Directory by default,
        and it can be tricky to make it work without losing the Directory structure.
        However the `[System.IO.Compression.ZipFile]::CreateFromDirectory()` method
        makes it possible, and this function is a wrapper for it.
        The reason for creating a wrapper is to simplify testing via mocking.

    .PARAMETER Path
        Path of the File or Directory to compress.

    .PARAMETER DestinationPath
        Destination file to Zip the Directory into.

    .PARAMETER CompressionLevel
        Compression level between Fastest, Optimal, and NoCompression.

    .PARAMETER IncludeBaseDirectory
        Whether you want the zip to include the Directory and its content in the zip,
        or if you only want the content of the Directory to be at the zip's root (default).

    .PARAMETER Force
        Delete the destination file if it already exists.

    .EXAMPLE
        PS C:\> Compress-ArchiveByDirectory -Path C:\MyDir -DestinationPath C:\MyDir.zip -CompressionLevel Fastest -IncludeBaseDirectory -Force

#>

function Compress-ArchiveByDirectory
{
    [CmdletBinding(
        SupportsShouldProcess = $true,
        ConfirmImpact = 'Medium'
    )]
    [OutputType([void])]
    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,

        [Parameter()]
        [System.Management.Automation.SwitchParameter]
        $Force
    )

    if (-not  (Split-Path -IsAbsolute -Path $DestinationPath))
    {
        $DestinationPath = Join-Path -Path (Get-Location -PSProvider fileSystem) -ChildPath $DestinationPath
    }

    if ($PSBoundParameters.ContainsKey('Force') -and $true -eq $PSBoundParameters['Force'])
    {
        if ((Test-Path -Path $DestinationPath) -and $PSCmdlet.ShouldProcess("Deleting Zip file '$DestinationPath'.", $DestinationPath, 'Remove-Item -Force'))
        {
            Remove-Item -Force $DestinationPath -ErrorAction Stop
        }
    }

    if ($PSCmdlet.ShouldProcess("Zipping '$Path' to '$DestinationPath' with compression level '$CompressionLevel', includig base dir: '$($IncludeBaseDirectory.ToBool())'.", $Path, 'ZipFile'))
    {
        [System.IO.Compression.ZipFile]::CreateFromDirectory($Path, $DestinationPath, $CompressionLevel, $IncludeBaseDirectory.ToBool())
    }
}
#EndRegion './Private/Compress-ArchiveByDirectory.ps1' 81
#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/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()]
    [Experimental('GuestConfiguration.SetScenario', 'Show')]
    [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

            $testResult = Test-DscConfiguration -ConfigurationName $packageName -Verbose:$verbose
            $getResult = @()
            $getResult = $getResult + (Get-DscConfiguration -ConfigurationName $packageName -Verbose:$verbose)

            $testResult.resources_not_in_desired_state | ForEach-Object {
                $resourceId = $_;
                if ($getResult.count -gt 1)
                {
                    for ($i = 0; $i -lt $getResult.Count; $i++)
                    {
                        if ($getResult[$i].ResourceId -ieq $resourceId)
                        {
                            $getResult[$i] = $getResult[$i] | Select-Object *, @{
                                n = 'complianceStatus'
                                e = { $false }
                            }
                        }
                    }
                }
                elseif ($getResult.ResourceId -ieq $resourceId)
                {
                    $getResult = $getResult | Select-Object *, @{
                        n = 'complianceStatus'
                        e = { $false }
                    }
                }
            }

            $testResult.resources_in_desired_state | ForEach-Object {
                $resourceId = $_
                if ($getResult.count -gt 1)
                {
                    for ($i = 0; $i -lt $getResult.Count; $i++)
                    {
                        if ($getResult[$i].ResourceId -ieq $resourceId)
                        {
                            $getResult[$i] = $getResult[$i] | Select-Object *, @{
                                n = 'complianceStatus'
                                e = { $true }
                            }
                        }
                    }
                }
                elseif ($getResult.ResourceId -ieq $resourceId)
                {
                    $getResult = $getResult | Select-Object *, @{
                        n = 'complianceStatus'
                        e = { $true }
                    }
                }
            }

            [PSCustomObject]@{
                complianceStatus = $testResult.compliance_state
                resources        = $getResult
            }
        }
        finally
        {
            $env:PSModulePath = $systemPSModulePath
        }
    }
}
#EndRegion './Public/Get-GuestConfigurationPackageComplianceStatus.ps1' 137
#Region './Public/Install-GuestConfigurationAgent.ps1' 0
function Install-GuestConfigurationAgent
{
    [CmdletBinding()]
    [Experimental('GuestConfiguration.SetScenario', 'Show')]
    [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)
            }
        }
    }
    else
    {
        Write-Verbose -Message "Guest Configuration Agent binaries already installed at '$gcBinPath', skipping."
    }
}
#EndRegion './Public/Install-GuestConfigurationAgent.ps1' 66
#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()]
    [Experimental('GuestConfiguration.SetScenario', 'Show')]
    [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
        {
            Write-Debug -Message "'$Path' is the Package Name."
            # The $Path parameter is the PackageName, no need to version check.
            # if package name is not installed, throw an error
            try
            {
                $installedPackagePath = Join-Path -Path $guestConfigurationPolicyPath -ChildPath $Path -Resolve
            }
            catch
            {
                throw "The Package '$Package' is not installed. Please provide the Path to the Zip or the URL to download the package from."
                return
            }
        }


        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

            if
            (   # none of the packages are versioned or the versions match, we're good
                -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' 148
#Region './Public/New-GuestConfigurationFile.ps1' 0

<#
    .SYNOPSIS
        Automatically generate a MOF file based on
        files discovered in a folder path

        This command is optional and is intended to
        reduce the number of steps needed when
        using other language abstractions such as Pester

        When creating packages from compiled DSC
        configurations, you do not need to run this command

    .Parameter Source
        Location of the folder containing content

    .Parameter Path
        Location of the folder containing content

    .Parameter Format
        Format of the files (currently only Pester is supported)

    .Parameter Force
        When specified, will overwrite the destination file if it already exists

    .Example
        New-GuestConfigurationFile -Path ./Scripts

    .OUTPUTS
        Return the path of the generated configuration MOF file
#>


function New-GuestConfigurationFile
{
    [CmdletBinding()]
    [Experimental("GuestConfiguration.Pester", "Show")]
    [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]
        $Source,

        [Parameter(Position = 2, Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Path,

        [Parameter(Position = 3, ValueFromPipelineByPropertyName = $true)]
        [System.String]
        $Format = 'Pester',

        [Parameter()]
        [System.Management.Automation.SwitchParameter]
        $Force
    )

    $return = [PSCustomObject]@{
        Name = ''
        Configuration = ''
    }

    if ('Pester' -eq $Format)
    {
        Write-Warning -Message 'Guest Configuration: Pester content is an expiremental feature and not officially supported'
        if ([ExperimentalFeature]::IsEnabled("GuestConfiguration.Pester"))
        {
            $ConfigMOF = New-MofFileforPester -Name $Name -PesterScriptsPath $Source -Path $Path -Force:$Force
            $return.Name = $Name
            $return.Configuration = $ConfigMOF.Path
        }
        else
        {
            throw 'Before you can use Pester content, you must enable the experimental feature in PowerShell.'
        }
    }

    return $return
}
#EndRegion './Public/New-GuestConfigurationFile.ps1' 86
#Region './Public/New-GuestConfigurationPackage.ps1' 0

<#
    .SYNOPSIS
        Creates a Guest Configuration policy package.

    .Parameter Name
        Guest Configuration package name.

    .Parameter Version
        Guest Configuration package Version (SemVer).

    .Parameter Configuration
        Compiled DSC configuration document full path.

    .Parameter Path
        Output folder path.
        This is an optional parameter. If not specified, the package will be created in the current directory.

    .Parameter ChefInspecProfilePath
        Chef profile path, supported only on Linux.

    .Parameter Type
        Specifies whether or not package will support AuditAndSet or only Audit. Set to Audit by default.

    .Parameter Force
        Overwrite the package files if already present.

    .Example
        New-GuestConfigurationPackage -Name WindowsTLS -Configuration ./custom_policy/WindowsTLS/localhost.mof -Path ./git/repository/release/policy/WindowsTLS

    .OUTPUTS
        Return name and path of the new Guest Configuration Policy package.
#>


function New-GuestConfigurationPackage
{
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    param
    (
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Name,

        [Parameter(Position = 1, Mandatory = $true, ParameterSetName = 'Configuration', ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Configuration,

        [Parameter(Position = 2, ParameterSetName = 'Configuration', ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [SemVer]
        $Version,

        [Parameter(ParameterSetName = 'Configuration')]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ChefInspecProfilePath,

        [Parameter(ParameterSetName = 'Configuration')]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $FilesToInclude,

        [Parameter()]
        [System.String]
        $Path = '.',

        [Parameter()]
        [Experimental('GuestConfiguration.SetScenario', 'Show')]
        [PackageType]
        $Type = 'Audit',

        [Parameter()]
        [System.Management.Automation.SwitchParameter]
        $Force
    )

    if (-not (Get-Variable -Name Type -ErrorAction SilentlyContinue))
    {
        $Type = 'Audit'
    }

    $verbose = ($PSBoundParameters.ContainsKey("Verbose") -and ($PSBoundParameters["Verbose"] -eq $true))
    $stagingPackagePath = Join-Path -Path (Join-Path -Path $Path -ChildPath $Name) -ChildPath 'unzippedPackage'
    $unzippedPackageDirectory = New-Item -ItemType Directory -Force -Path $stagingPackagePath
    $Configuration = Resolve-Path -Path $Configuration

    if (-not (Test-Path -Path $Configuration -PathType Leaf))
    {
        throw "Invalid mof file path, please specify full file path for dsc configuration in -Configuration parameter."
    }

    Write-Verbose -Message "Creating Guest Configuration package in temporary directory '$unzippedPackageDirectory'"

    # Verify that only supported resources are used in DSC configuration.
    Test-GuestConfigurationMofResourceDependencies -Path $Configuration -Verbose:$verbose

    # Save DSC configuration to the temporary package path.
    $configMOFPath = Join-Path -Path $unzippedPackageDirectory -ChildPath "$Name.mof"
    Save-GuestConfigurationMofDocument -Name $Name -SourcePath $Configuration -DestinationPath $configMOFPath -Verbose:$verbose

    # Copy DSC resources
    Copy-DscResources -MofDocumentPath $Configuration -Destination $unzippedPackageDirectory -Verbose:$verbose -Force:$Force

    # Modify metaconfig file
    $metaConfigPath = Join-Path -Path $unzippedPackageDirectory -ChildPath "$Name.metaconfig.json"
    Update-GuestConfigurationPackageMetaconfig -metaConfigPath $metaConfigPath -Key 'Type' -Value $Type.ToString()

    if ($PSBoundParameters.ContainsKey('Version'))
    {
        Update-GuestConfigurationPackageMetaconfig -MetaConfigPath $metaConfigPath -key 'Version' -Value $Version.ToString()
    }

    if (-not [string]::IsNullOrEmpty($ChefInspecProfilePath))
    {
        # Copy Chef resource and profiles.
        Copy-ChefInspecDependencies -PackagePath $unzippedPackageDirectory -Configuration $Configuration -ChefInspecProfilePath $ChefInspecProfilePath
    }

    # Copy FilesToInclude
    if (-not [string]::IsNullOrEmpty($FilesToInclude))
    {
        $modulePath = Join-Path $unzippedPackageDirectory 'Modules'
        if (Test-Path $FilesToInclude -PathType Leaf)
        {
            Copy-Item -Path $FilesToInclude -Destination $modulePath  -Force:$Force
        }
        else
        {
            $filesToIncludeFolderName = Get-Item -Path $FilesToInclude
            $FilesToIncludePath = Join-Path -Path $modulePath -ChildPath $filesToIncludeFolderName.Name
            Copy-Item -Path $FilesToInclude -Destination $FilesToIncludePath -Recurse -Force:$Force
        }
    }

    # Create Guest Configuration Package.
    $packagePath = Join-Path -Path $Path -ChildPath $Name
    $null = New-Item -ItemType Directory -Force -Path $packagePath
    $packagePath = Resolve-Path -Path $packagePath
    $packageFilePath = join-path -Path $packagePath -ChildPath "$Name.zip"
    if (Test-Path -Path $packageFilePath)
    {
        Remove-Item -Path $packageFilePath -Force -ErrorAction SilentlyContinue
    }

    Write-Verbose -Message "Creating Guest Configuration package : $packageFilePath."
    Compress-ArchiveByDirectory -Path $unzippedPackageDirectory -DestinationPath $packageFilePath -Force:$Force

    [pscustomobject]@{
        PSTypeName = 'GuestConfiguration.Package'
        Name = $Name
        Path = $packageFilePath
    }
}
#EndRegion './Public/New-GuestConfigurationPackage.ps1' 157
#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()]
        [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
            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' 200
#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()]
    [Experimental('GuestConfiguration.SetScenario', 'Show')]
    [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' 120
#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 "Testing ConfigurationName '$packageName'."
        $testResult = Test-DscConfiguration -ConfigurationName $packageName -Verbose:$verbose
        Write-Verbose -Message "Getting Configuration resources status."
        $getResult = @()
        $getResult = $getResult + (Get-DscConfiguration -ConfigurationName $packageName -Verbose:$verbose)

        Write-Debug -Message "Processing Resources not in Desired state."
        $testResult.resources_not_in_desired_state | ForEach-Object {
            $resourceId = $_;
            if ($getResult.count -gt 1)
            {
                for ($i = 0; $i -lt $getResult.Count; $i++)
                {
                    if ($getResult[$i].ResourceId -ieq $resourceId)
                    {
                        $getResult[$i] = $getResult[$i] | Select-Object *, @{
                            n = 'complianceStatus'
                            e = { $false }
                        }
                    }
                }
            }
            elseif ($getResult.ResourceId -ieq $resourceId)
            {
                $getResult = $getResult | Select-Object *, @{
                    n = 'complianceStatus'
                    e = { $false }
                }
            }
        }

        $testResult.resources_in_desired_state | ForEach-Object {
            $resourceId = $_
            if ($getResult.count -gt 1)
            {
                for ($i = 0; $i -lt $getResult.Count; $i++)
                {
                    if ($getResult[$i].ResourceId -ieq $resourceId)
                    {
                        $getResult[$i] = $getResult[$i] | Select-Object *, @{
                            n = 'complianceStatus'
                            e = { $true }
                        }
                    }
                }
            }
            elseif ($getResult.ResourceId -ieq $resourceId)
            {
                $getResult = $getResult | Select-Object *, @{
                    n = 'complianceStatus'
                    e = { $true }
                }
            }
        }

        [PSCustomObject]@{
            complianceStatus = $testResult.compliance_state
            resources        = $getResult
        }
    }
    finally
    {
        $env:PSModulePath = $systemPSModulePath
    }
}
#EndRegion './Public/Test-GuestConfigurationPackage.ps1' 168