ACGCore.psm1


# START: source\.bootstrap.ps1
# Setting Random Number Generation:
$osInfo = Get-WmiObject Win32_OperatingSystem
$seed = ($osInfo.FreePhysicalMemory + $osInfo.NumberOfProcesses + [datetime]::Now.Ticks) % [int]::MaxValue
$script:__RNG = New-Object System.Random $seed
Remove-Variable 'seed'

# Initializeing Render-Template variables:
$script:__InterpolationTags = @{
    Start = '<<'
    End = '>>'
}
$script:__InterpolationTagsHistory = New-Object System.Collections.Stack


# Creating aliases:
New-Alias -Name '~' -Value Unlock-SecureString

# Backwards-compatibility aliases:
New-Alias -Name 'Save-Credential'   -Value 'Save-PSCredential'
New-Alias -Name 'Load-Credential'   -Value 'Restore-PSCredential'
New-Alias -Name 'Load-PSCredential' -Value 'Restore-PSCredential'
New-Alias -Name 'Parse-ConfigFile'  -Value 'Read-ConfigFile'
New-Alias -Name 'Create-Shortcut'   -Value 'New-Shortcut'
New-Alias -Name 'Render-Template'   -Value 'Format-Template'
New-Alias -Name 'Run-Operation'     -Value 'Invoke-ShoutOut'
New-Alias -Name 'Query-RegValue'    -Value 'Get-RegValue'
New-Alias -NAme 'Steal-RegKey'      -Value 'Set-RegKeyOwner'
# END: source\.bootstrap.ps1


# START: source\New-Shortcut.ps1
# New-Shortcut.ps1

function New-Shortcut(){
    param(
        [parameter(Mandatory=$true, position=1)][String]$ShortcutPath,
        [parameter(Mandatory=$true, position=2)][String]$TargetPath,
        [parameter(Mandatory=$false, position=3)][String]$Arguments,
        [parameter(Mandatory=$false, position=4)][String]$IconLocation
    )

    if ($ShortcutPath -match '^(?<directory>([A-Z]:|\.)[\\/]([^\\/]+[\\/])*)(?<filename>.*\.lnk)$'){
        $shortcutDir  = $Matches.directory
        $shortcutFile = $Matches.filename
    } else {
        return $false
    }

    $WSShell = New-Object -ComObject WScript.shell
    $shortcut = $WSShell.CreateShortcut($ShortcutPath)
    $shortcut.TargetPath = $TargetPath
    if ($Arguments) { $shortcut.Arguments = $Arguments }
    if ($IconLocation) { $shortcut.IconLocation = $IconLocation }
    $shortcut.Save()
    return $true

}
# END: source\New-Shortcut.ps1


# START: source\RegexPatterns.ps1
$Script:RegexPatterns = @{ }

$Script:RegexPatterns.IPv4AddressByte = "(25[0-4]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])" # A byte in an IPv4 Address
$IPv4AB = $Script:RegexPatterns.IPv4AddressByte
$Script:RegexPatterns.IPv4NetMaskByte = "(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[1-9])" # A non-full byte in a IPv4 Netmask
$IPv4NMB = $Script:RegexPatterns.IPv4NetMaskByte

$ItemChars = "[^\\/:*`"|<>]"
$Script:RegexPatterns.Directory = '(?<directory>(?<root>[A-Z]+:|\.|\\.*)[\\/]({0}+[\\/]?)*)' -f $ItemChars
$Script:RegexPatterns.File = ( '(?<file>(?<directory>((?<root>[A-Z]+:|\.|\\.*)[\\/])?({0}+[\\/])*)(?<filename>([^\\/]+)+(\.(?<extension>[^\\/.]+)?))' + ")" ) -f $ItemChars
$Script:RegexPatterns.IPv4Address = "($IPv4AB\.){3}$($IPv4AB)"
$Script:RegexPatterns.IPv4Netmask = "((255\.){3}$IPv4NMB)|((255\.){2}($IPv4NMB\.)0)|((255\.){1}($IPv4NMB\.)0\.0)|(($IPv4NMB\.)0\.0\.0)|0\.0\.0\.0"
$Script:RegexPatterns.GUID = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{10}"


<#
.SYNOPSIS
Returns the ACGCore regular expression with the given name.
#>

function Get-ACGCoreRegexPattern {
    param([string]$PatternName)

    if ($Script:RegexPatterns.ContainsKey($PatternName)) {
        return $Script:RegexPatterns[$PatternName]
    } else {
        throw "Invalid pattern name provided"
    }
}

<#
.SYNOPSIS
Rreturns the name of all standard regular expressions used in ACGCore.
#>

function Get-ACGCoreRegexPatternNames {

    return $Script:RegexPatterns.Keys
}


<#
.SYNOPSIS
Matches ACGCore regular expressions against a string.
.DESCRIPTION
Tries to match the given string $value against the pattern named $PatternName.
 
Returns a record of the match if the regex matches the given value (equivalent
to $matches), otherwise returns $false.
 
By default the this function assumes that the entire string should match the
given pattern. This behavior can be overriden by using the AllowPartialMatches
switch, in which case the function will attempt to match any part of the given
string.
#>

function Test-ACGCoreRegexPattern {
    param([string]$Value, [string]$PatternName, [switch]$AllowPartialMatch)

    try {
        $pattern = Get-ACGCoreRegexPattern $PatternName
        
        if (!$AllowPartialMatch) {
            $pattern = "^$pattern$"
        }

        if ($value -match $pattern) {
            return $matches.Clone()
        }

        return $false
    } catch {
        return $false
    }

}
# END: source\RegexPatterns.ps1


# START: source\Reset-Module.ps1
function Reset-Module {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ArgumentCompleter({
            Get-Module | Foreach-Object Name
        })]
        [string]$Name
    )

    if ($module = Get-Module $Name) {
        Remove-Module $module -Force
    }

    Import-Module $Name -Global
}
# END: source\Reset-Module.ps1

# ACGCore.conditions

# START: source\conditions\Test-Condition.ps1
function Test-Condition{
    param(
        [Parameter(Mandatory=$true,  Position=1)][scriptblock]$Test,
        [Parameter(Mandatory=$false, Position=2)][scriptblock]$OnPass=$null,
        [Parameter(Mandatory=$false, Position=3)][scriptblock]$OnFail=$null,
        [Parameter(Mandatory=$false, Position=4)][scriptblock]$Evaluate = { param($v) $true -eq $v }
    )

    $r = & $Test
    $pass = & $Evaluate $r

    if ($pass) {
        if ($OnPass) { & $OnPass }
    } else {
        if ($OnFail) { & $OnFail }
    }

    return $pass

}
# END: source\conditions\Test-Condition.ps1


# START: source\conditions\Wait-Condition.ps1
function Wait-Condition{
    param(
        [Parameter(Mandatory=$true,  Position=1)][scriptblock]$Test,
        [Parameter(Mandatory=$false, Position=2)][scriptblock]$OnPass=$null,
        [Parameter(Mandatory=$false, Position=3)][scriptblock]$OnFail=$null,
        [Parameter(Mandatory=$false, Position=4)][scriptblock]$Evaluate = { param($v) $true -eq $v },
        [Parameter(Mandatory=$false, Position=5)][int]$IntervalMS=200,
        [Parameter(Mandatory=$false, Position=6)][int]$TimeoutMS=0
    )

    $__waitStart__ = [datetime]::Now
    do {
        if ($TimeoutMS -gt 0) {
            $t = ([datetime]::Now - $__waitStart__).TotalMilliSeconds
            if ($t -gt $TimeOutMS) {
                if ($OnFail) { & $OnFail }
                return $false
            }
        }

        Start-Sleep -MilliSeconds $IntervalMS
        $r = Test-Condition -Test $Test -Evaluate $Evaluate
    } while(!$r)

    if ($OnPass) { & $OnPass }

    return $true

}
# END: source\conditions\Wait-Condition.ps1

# ACGCore.configfiles

# START: source\configfiles\Read-ConfigFile.ps1
<#
.SYNOPSIS
Parsing function used for ACGroup-style .ini configuration files.
 
.DESCRIPTION
Used to parse ACGroup-style .ini files.
 
Grammar:
    file -> <lines>
    lines -> <line> | <line><lines>
    line -> <include> | <section header> | <declaration> | <comment> | <empty>
    include -> is<comment>
    section header -> sh<comment>
    declarations -> sd<comment>
    comment -> c
    empty -> e
 
Terminals:
    is: Include Statement
        ^#include\s[^\s#]+
 
    sh: Section Header
        ^\s*\[[^\]]+\]
 
    sd: Setting Declaration
        ^\s*[^\s=#]+\s*(=\s*([^#]|\\#)+|`"[^`"]*`"|'[^']*')?
 
    c: Comment
        (?<![\\])#.*
 
    e: Empty line
        \s*
 
Additional Rules:
    - The first declaration of the file must be preceeded by a section header.
    - If more than one value is declared for a setting, they will be collected
      into an array.
    - All values will be read as strings and the application using the
      configuration must determine how to interpret the values.
 
.PARAMETER Path
The path to the configuration file.
 
.PARAMETER Content
Alternatively content to be parsed can be provided as a string.
 
.PARAMETER Config
Pre-populated configuration hashtable. If provided, the parser will add new settings to the hashtable.
The default behavior is to generate a new hashtable.
 
.PARAMETER NoInclude
Causes the parser skip include statements.
 
.PARAMETER NotStrict
Stops the parser from throwing an exceptions when errors are encountered.
 
.PARAMETER Silent
Stops the parser from outputting anything to the console.
 
.PARAMETER MetaData
Hashtable used to record MetaData while parsing.
Presently only records Includes and errors.
 
.PARAMETER Cache
Hashtable used to cache the results of each file parsed. Useful to minimize
reads from disk when parsing multiple job files using the common includes.
 
.PARAMETER Loud
Causes the parser to output extra information to the console.
 
.PARAMETER duplicatesAllowed
Names of settings for which duplicate values are allowed.
 
By default, if there are two declarations of the same setting with the same value,
the second occurence of the value will be discarded. When a setting name is
specified here, the second occurrence will instead be appended to the list of
values for the setting.
 
.PARAMETER IncludeRootPath
The root path to use when resolving includes. If this value isn't provided
then it will default to the directory part of $Path.
 
Include-paths that start with '\' or '/' will use this value when resolving
where to look for the included file.
 
Paths that do not start with either '\' or '/' will use the directory of the
file currently being processed.
 
If the command is called using the "String" parameter set, then this value will
default to $pwd (current working directory).
 
All included files will be parsed using the same IncludeRootPath.
 
.EXAMPLE
Normal Read:
    $conf = Parse-Config "C:\Config.ini"
 
Accumulating information into a configuration hashtable:
    $conf = Parse-Config "C:\Config2.ini" $config
 
Skipping #include statements:
    $conf = Parse-Config "C:\Config.ini" -NoInclude
 
Stop the parser from throwing an exception on error (use MetaData object to record errors):
    $metadata = @{}
    $conf = Parse-Config "C:\Config.ini" -NotStrict -MetaData $metadata
    # Echo out the errors:
    $metadata.Errors | % { Write-Host $_ }
 
.NOTES
General notes
#>

function Read-ConfigFile {
    [CmdletBinding(DefaultParameterSetName="File")]
    param (
        [parameter(
            Mandatory=$true,
            Position=1,
            ParameterSetName="File",
            HelpMessage="Path to the file."
        )] [String] $Path,              # Name of the job-file to parse (including extension)
        [parameter(
            Mandatory=$true,
            Position=1,
            ParameterSetName="String",
            HelpMessage="Content to be parsed instead of reading from the file path. If this option is used and the path is not an actual file path, then 'IncludeRootPath' MUST be specified. Path must be specified regardless."
        )] [string]$Content,
        [parameter(
            Mandatory=$false,
            Position=2,
            HelpMessage="Pre-populated configuration hashtable. If provided, any options read from the given file will be appended."
        )] [Hashtable] $Config = @{},  # Pre-existing configuration, if given we'll simply add to this one.
        [parameter(
            Mandatory=$false,
            HelpMessage="Tells the parser to skip include stetements."
        )] [Switch] $NoInclude,                   # Tells the parser to skip any include statements
        [Parameter(
            Mandatory=$false,
            HelpMessage="Tells the parser not to throw an exception on parsing errors."
        )] [Switch] $NotStrict,                   # Tells the parser to not generate any exceptions.
        [Parameter(
            Mandatory=$false,
            HelpMessage="Suppresses all command-line output from the parser."
        )] [Switch] $Silent,                      # Supresses all commandline-output from the parser.
        [parameter(
            Mandatory=$false,
            HelpMessage='Hashtable used to record MetaData. Includes will be recorded in $MetaData.Includes.'
        )] [Hashtable] $MetaData,                 # Hashtable used to capture MetaData while parsing.
                                                  # This will record Includes as '$MetaData.includes'.
        [Parameter(
            Mandatory=$false,
            HelpMessage='Hashtable used to cache includes to minimize reads from disk when rapidly parsing multiple files using common includes.'
        )][Hashtable] $Cache,
        [parameter(
            Mandatory=$false,
            HelpMessage="Causes the Parser to output extra information to the console."
        )] [Switch] $Loud,                        # Equivalent of $Verbose
        [parameter(
            Mandatory=$false,
            HelpMessage="Array of settings for which values can be duplicated."
        )] [array]
        $duplicatesAllowed = @("Operation","Pre","Post"),                    # Declarations for which duplicate values are allowed.
        [parameter(
            Mandatory=$false,
            HelpMessage="The root directory used to resolve includes. Defaults to the directory of the config file."
        )] [string]$IncludeRootPath               # The root directory used to resolve includes.
    )
    
    # Error-handling specified here for reusability.
    $handleError = {
        param(
            [parameter(Mandatory=$true)] [String] $Message
        )
        if ($MetaData) {
            $MetaData.Errors = $Message
        }
        
        if ($NotStrict) {
            if (!$Silent) { write-host $Message -ForegroundColor Red }
        } else {
            throw $Message
        }
    }

    $Verbose = if (($Verbose -or $Loud) -and !$Silent) { $true } else { $false }
    

    switch ($PSCmdlet.ParameterSetName) {
    
        "File" {
            if( $Path -and ([System.IO.File]::Exists($Path)) ) {
                $lines = [System.IO.File]::ReadAllLines($Path, [System.Text.Encoding]::UTF8)
            } else {
                . $handleError -Message "<InvalidPath>The given path doesn't lead to an existing file: '$Path'"
                return
            }

            $currentDir = [System.IO.Directory]::GetParent($Path)

        }

        "String" {
            $lines = $Content -split "`n"
            $currentDir = "$pwd"
        }
    }

    if (!$PSBoundParameters.ContainsKey("IncludeRootPath")) {
        $IncludeRootPath = $currentDir
    }

    $conf = @{}
    if ($Config) { # Protect against NULL-values.
        $conf = $Config
    }

    if ($MetaData) {
        if (!$MetaData.Includes) { $MetaData.Includes = @() }
        if (!$MetaData.Errors)   { $MetaData.Errors = @() }
    }
    
    $regex = @{ }
    $regex.Comment = "(?<![\\])#(?<comment>.*)"
    $regex.Include = "^#include\s+(?<include>[^\s#]+)\s*($($regex.Comment))?$"
    $regex.Heading = "^\s*\[(?<heading>[^\]]+)\]\s*($($regex.Comment))?$"
    $regex.Setting = "^\s*(?<name>[^\s=#]+)\s*(=\s*(?<value>([^#]|\\#)+|`"[^`"]*`"|'[^']*'))?\s*($($regex.Comment))?$"
    $regex.Entry   = "^\s*(?<entry>.+)\s*"
    $regex.Empty   = "^\s*($($regex.Comment))?$"  

    $linenum = 0
    $CurrentSection = $null
    foreach($line in $lines) {
        $linenum++
        switch -Regex ($line) {
            $regex.Include {
                if ($Verbose) {
                    write-host -ForegroundColor Green "Include: '$line'";
                    Write-Host "------[Start:$($Matches.include)]".PadRight(80, "-")
                }
                if ($NoInclude) { continue }
                if ($MetaData) { $MetaData.includes += $Matches.include }
                
                $includePath = $Matches.include
   
                $parseArgs = @{
                    Config=$conf;
                    MetaData=$MetaData;
                    Cache=$Cache
                    IncludeRootPath=$IncludeRootPath;
                }

                if ($includePath -match "^[/\\]") {
                    $parseArgs.Path = "$IncludeRootPath${includePath}.ini" # Absolute path.
                } else {
                    $parseArgs.Path = "$currentDir\${includePath}.ini"; # Relative path.
                }

                if ($PSBoundParameters.ContainsKey("Verbose")) { $parseArgs.Verbose = $Verbose }
                if ($PSBoundParameters.ContainsKey("NotStrict")) { $parseArgs.NotStrict = $NotStrict }
                if ($PSBoundParameters.ContainsKey("Silent"))  { $parseArgs.Silent = $Silent }

                try {

                    if ($Cache) {
                        $parseArgs.Remove("Config")
                        if ($Cache.ContainsKey($parseArgs.Path)) {
                            if ($Loud) { Write-Host "Found include file in the cache!" -ForegroundColor Green }
                            $ic = $Cache[$parseArgs.Path]
                        } else {
                            if ($Loud) { Write-Host "include file not found in the cache, parsing file..." -ForegroundColor Yellow }
                            $ic = Parse-ConfigFile @parseArgs
                            $Cache[$parseArgs.Path] = $ic
                        }
                        $conf = Merge-Configs $conf $ic -duplicatesAllowed $duplicatesAllowed
                    } else {
                        Parse-ConfigFile @parseArgs | Out-Null
                    }
                    
                } catch {
                    if ($_.Exception -like "<InvalidPath>*") {
                        . $handleError -Message $_
                    } else {
                        . $handleError "An unknown exception occurred while parsing the include file at '$($parseArgs.Path)' (in root file '$Path'): $_"
                    }
                }

                if ($Verbose) {
                    Write-Host "------[End:$includePath]".PadRight(80, "-")
                }
                break;
            }
            $regex.Heading {
                if ($Verbose) {  write-host -ForegroundColor Green "Heading: '$line'"; }
                $CurrentSection = $Matches.Heading
                if (!$conf[$Matches.Heading]) {
                    $conf[$Matches.Heading] = @{ }
                } 
                break;
            }
            $regex.Setting {
                if (!$CurrentSection) {
                    . $handleError -Message "<OrphanSetting>Ecountered a setting before any headings were declared (line $linenum in '$Path'): '$line'"
                }

                if ($Verbose) { Write-Host -ForegroundColor Green "Setting: '$line'"; }
                $value = $Matches.Value -replace "\\#","#" # Strip escape character from literal '#'s
                if ($conf[$CurrentSection][$Matches.Name]) {
                    if ($conf[$CurrentSection][$Matches.Name] -is [Array]) {
                        if ( ($Matches.Name -in $duplicatesAllowed) -or (-not $conf[$CurrentSection][$Matches.Name].Contains($value)) ) {
                            $conf[$CurrentSection][$Matches.Name] += $value
                        }
                    } else {
                        $conf[$CurrentSection][$Matches.Name] = @( $conf[$CurrentSection][$Matches.Name], $value )
                    }
                } else {
                    $v = if ($null -eq $value) { "" } else { $value } # Convertion to match the behaviour of Read-Conf
                    $conf[$CurrentSection][$Matches.Name] = $v
                }
                break;
            }
            $regex.Empty   {
                if ($Verbose) {  Write-Host -ForegroundColor Green "Empty: '$line'"; }
                break;
            }
            default {
                . $handleError "<MalformedLine>Found an unrecognizable line (line $linenum in $path): $line"
                break;
            }
        }
    }

    if ($Cache) {
        $Cache[$Path] = $conf
    }

    return $conf
}

function Merge-Configs {
    param(
        [Parameter(Mandatory=$true,  HelpMessage="Configuration 1, values from this object will appear first in the cases where values overlap.")]
        [ValidateNotNull()][hashtable]$C1,
        [Parameter(Mandatory=$true,  HelpMessage="Configuration 2, values from this object will appear last in the cases where values overlap.")]
        [ValidateNotNull()][hashtable]$C2,
        [parameter(Mandatory=$false, HelpMessage="Array of settings for which values can be duplicated.")]
        [array] $duplicatesAllowed = @("Operation","Pre","Post")
    )

    $combineValues = {
        param($n, $v1, $v2)

        $da = $n -in $duplicatesAllowed

        if ($v1 -is [array]) {
            if ($v2 -isnot [array]) {
                if (!$da -and ($v2 -in $v1)) {
                    return $v1
                }
                return $v1 + $v2
            } else {
                $v = $v1
                $v2 | Where-Object {
                    $da -or $_ -notin $v
                } | ForEach-Object {
                    $v += $_
                }
                return $v
            }
        } else {
            if ($v2 -isnot [Array] ) {
                if (!$da -and $v1 -eq $v2) {
                    return $v1
                }
                return @($v1, $v2)
            } else {
                $v = @($v1)
                $v2 | Where-Object {
                    $da -or $_ -notin $v
                } | ForEach-Object { $v += $_ }
                return $v
            }
        } 
    }

    $NC = @{}

    $C1.Keys | Where-Object {
        $_ -and ($C1[$_] -is [hashtable])
    } | ForEach-Object {
        $s = $_
        $NC[$s] = @{}
        $C1[$s].GetEnumerator() | ForEach-Object {
            $NC[$s][$_.Name] = $C1[$s][$_.Name]
        }
    }
    $C2.Keys | Where-Object {
        $_ -ne $null -and ($C2[$_] -is [hashtable])
    } | ForEach-Object {
        $s = $_
        if (!$NC.ContainsKey($s)) {
            $NC[$s] = @{}
        }
        $C2[$s].GetEnumerator() | ForEach-Object {
            $n = $_.Name
            $v = $_.Value

            if (!$NC[$s].ContainsKey($n)) {
                $NC[$s][$n] = $v
                return
            }

            $NC[$s][$n] = . $combineValues $n $NC[$s][$n] $v
        }
    }
    
    return $NC
}
# END: source\configfiles\Read-ConfigFile.ps1


# START: source\configfiles\Write-ConfigFile.ps1
function Write-ConfigFile {
    param(
        [hashtable]$Config,
        [string]$Path
    )

    [string[]]$output = @()
    $keys = $Config.keys

    $keys = $Keys | Sort-Object

    foreach ($key in $keys) {
        $output += "[$key]"
        foreach ($item in $config[$key].keys) {
            foreach ($value in $config[$key][$item]) {
                if ($null, "" -contains $value) {
                    # Entry, just append it to the output
                    $output += $item
                    continue
                }

                # Setting, Append <item>=<value> to output for each value.
                if ($value -is [string]) {
                    $value = $value.Replace("#", "\#").trimend()
                }
                $output += "{0}={1}" -f $item, $value
            }
        }
        $output += "" # Empty line between each section to make output more readable.
    }


    if ($PSBoundParameters.ContainsKey('Path')) {
        if (!(test-path $Path)) {
            new-item -itemtype file -force -Path $Path | out-null
        }
        [System.IO.File]::WriteAllLines($Path, $output, [System.Text.Encoding]::UTF8)
        #$output | out-file -force -filepath $Path
    }
    else {
        $output
    }

}
# END: source\configfiles\Write-ConfigFile.ps1

# ACGCore.credentials

# START: source\credentials\ConvertFrom-PSCredential.ps1

<#
.SYNOPSIS
Converts a PSCredential object into a portable string representation.
 
.DESCRIPTION
Converts a PSCredential object into a portable string represenation.
 
If the $UseKey switch is specified, the function returns a hashtable with the
following keys:
    - Key: The key used to encrypt the credential.
    - Credential: The encrypted string representation of the credential.
 
Otherwise the encrypted string representation is returned.
 
.PARAMETER Credential
PSCredential Object to convert.
 
.PARAMETER UseKey
Switch to indicate that the credential is to be encrypted using a key.
 
.PARAMETER Key
A base64-encoded key of 256 bits that should be used when encrypting the credential (DPAPI).
 
.PARAMETER Thumbprint
Thumbprint of a certificate in the certificate store of the local machine.
 
This will cause the the password to be encrypted using the certificates public key.
 
The Cmdlet will look in the entire store for a certificate with the given thumbprint.
 
If more than 1 certificate is found with the thumbprint, the Cmdlet will verify that they
are in fact duplicate copies of the same certificate by check that they have the same Issuer
and Serial Number.
 
If more than 1 certificate are found to have the same thumbprint this Cmdlet will throw
an exception.
 
WARNING: You will need to use the private key associated with the certificate to
decrypt the credential. This Cmdlet does not check if the private key is available.
 
#>

function ConvertFrom-PSCredential {
    [CmdletBinding(DefaultParameterSetName="dpapi")]
    param(
        [Parameter(Mandatory=$true, Position=1, ValueFromPipeline=$true, HelpMessage="Credential to convert.")]
        [PSCredential] $Credential,
        [Parameter(Mandatory=$true, ParameterSetName='dpapi.key', HelpMessage='Signals that the credential should be protected using a DPAPI key.')]
        [switch] $UseKey,
        [Parameter(Mandatory=$false, ParameterSetName='dpapi.key', HelpMessage='A base64 encoded key to use when encrypting the credentials. If this parameter is not specified when the $UseKey switch is set, a random 256 bit key will be generated.')]
        [string] $Key,
        [Parameter(Mandatory=$true, ParameterSetName='x509.managed', HelpMessage='Thumbprint of the certificate that should be used to encrypt the credential. Warning: you will need the corresponding private key to decrypt the credential.')]
        [string] $Thumbprint,
        [Parameter(Mandatory=$true, ParameterSetName='plain', HelpMessage='Disable encryption, causing the plain text be base64 encoded.')]
        [switch] $NoEncryption,
        [Parameter(Mandatory=$true, ParameterSetName='plain', HelpMessage='Are you completely sure you do not want to use encryption?.')]
        [switch] $ThisIsNotProductionCode,
        [Parameter(Mandatory=$true, ParameterSetName='plain', HelpMessage="Ok, you're the boss.")]
        [switch] $IKnowWhatIAmDoing
    )
    
    # Scriptblock to convert string to Base64 string.
    $convertStringToBase64 = {
        param($s)

        $b      = [System.Text.Encoding]::Default.GetBytes($s)
        $utf8b  = [System.Text.Encoding]::Convert([System.Text.Encoding]::Default, [System.Text.Encoding]::UTF8, $b)
        $b64s   = [convert]::ToBase64String($utf8b)

        return $b64s
    }

    $result = @{}
    $header = @{}
    $username = $Credential.UserName
    $secPassword = $Credential.Password
    $encPassword = $null

    switch ($PSCmdlet.ParameterSetName) {
        dpapi       {
            $header.m = 'dpapi'

            $encPassword = ConvertFrom-SecureString -SecureString $secPassword
        }

        dpapi.key   {
            $header.m = 'dpapi.key'
            
            $convertArgs = @{
                SecureString    = $secPassword
                UseDPAPIKey     = $true
            }

            if ($PSBoundParameters.ContainsKey($Key)) {
                $convertArgs.Key = $Key
            }

            $r = Export-SecureString @ConvertArgs

            $result.Key = $r.Key
            $encPassword = $r.String
        }

        x509.managed {
            <#
                Encrypt the credential using public key encryption via a X509 certificate found in Windows Certificate Store.
 
                Headers:
                    - m: Method ('x509.managed')
                    - t: Thumbprint of the certificate used.
            #>

            $header.m = 'x509.managed'
            $header.t = $Thumbprint

            $encPassword = convertTo-CertificateSecuredString -SecureString $Credential.Password -Thumbprint $Thumbprint
            
        }

        plain {
            $header.m = 'plain'
            $encPassword = & $convertStringToBase64 (Unlock-SecureString $secPassword)
        }
    }

    $headerString = $header | ConvertTo-Json -Compress

    $credStr = @(
        & $convertStringToBase64 $headerString
        & $convertStringToBase64 $username
        $encPassword
    ) -join ":"

    if ($result.Count -eq 0) {
        $result = $credStr
    } else {
        $result.CredentialString = $credStr
    }

    return $result
}
# END: source\credentials\ConvertFrom-PSCredential.ps1


# START: source\credentials\ConvertTo-PSCredential.ps1
<#
.SYNOPSIS
Converts a portable string representation of a PSCredential object back into a PSCredential Object.
 
.DESCRIPTION
Converts a portable string reprentation of a PSCredential object back into a PSCredential Object.
 
Most strings contain all the information required for decryption, so this Cmdlet does not expose many parameters.
 
.PARAMETER CredentialString
The string representation of the PSCredential object to restore.
 
.PARAMETER Key
A base64 key (256 bits) that should be used to decrypt the stored credential.
 
.OUTPUTS
[SecureString]
#>

function ConvertTo-PSCredential {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, HelpMessage="String representation of the credential to restore.")]
        [string]$CredentialString,
        [Parameter(Mandatory=$false, HelpMessage="DPAPI key to use when decrypting the credential (256 bits, base64 encoded).")]
        [string]$Key
    )

    $base64StrRegex = '[A-Za-z-0-9+\/]+={0,2}'
    $credStringRegex = '^(?<h>{0}):(?<u>{0}):(?<p>{0})$' -f $base64StrRegex
    $legacyCredStringRegex = '^(?<u>[^:]+):(?<p>{0})$' -f $base64StrRegex

    switch -Regex ($CredentialString) {
        $credStringRegex {

            $fields = @{
                header = $Matches.h
                username = $Matches.u
                password = $Matches.p
            }

            $headerBytes   = [Convert]::FromBase64String($fields.header)
            $headerString  = [System.Text.Encoding]::UTF8.GetString($headerBytes)

            try {
                $header = ConvertFrom-Json $headerString -ErrorAction Stop
            } catch {
                $msg = "Failed to read header field: '{0}'" -f $headerString
                $ex = New-Object System.Exception $msg $_.Exception
                throw $ex
            }

            if ($header -isnot [PSCustomObject]) {
                throw "Invalid header field format: $headerString"
            }

            if ($null -eq $header.m) {
                throw "Missing method ('m') field in header field: $headerString"
            }

            $secPassword = switch ($header.m) {
                dpapi {
                    # Implicit encryption using user credentials. This only works on Windows.
                    # Assumption: The string was produced using ConvertFrom-SecureString Cmdlet.

                    Import-SecureString -String $fields.password
                }

                dpapi.Key {
                    # Explicit encryption using DPAPI with a key (128, 192 or 256 bits). This only works on Windows.
                    # Assumption: The string was produced using ConvertFrom-PSCredential Cmdlet with the 'Key' parameter.

                    if (-not $PSBoundParameters.ContainsKey('Key')) {
                        throw "Credential is DPAPI key-encrypted, but no value provided for 'Key' parameter."
                    }

                    Import-SecureString -String $fields.password -DPAPIKey $Key
                }

                x509.managed {
                    # Explicit encryption using a X509 certificate found in the certificate store on this computer.
                    # NOTE: The private key associated with the certificate must be available, otherwise the decryption will fail.

                    if ($null -eq $header.t) {
                        $msg = "Unable to decrypt credential: Invalid credential string header. Method '{0}' specified but thumbprint is missing (no 't' field)" -f $_
                        throw $msg
                    }

                    try {
                        Import-SecureString -String $fields.password -Thumbprint $header.t -ErrorAction Stop
                    } catch {
                        $msg = "Failed to decrypt the credential using certificat (Thumbprint: {0}). See inner exception for details." -f $header.t
                        $ex = New-Object System.Exception $msg, $_.Exception
                        throw $ex
                    }
                }
                
                plain {
                    # Plain Text encyption, this is only available for debug/testing/demo purposes.
                    $passBytes  = [Convert]::FromBase64String($fields.password)
                    $passString = [System.Text.Encoding]::UTF8.GetString($passBytes)
                    Import-SecureString -String $passString -NoEncryption
                }

                default {
                    $msg = "Unrecognized encryption method in credential header ('{0}'). This may indicate that you are using an out-dated version of the Cmdlet." -f $header.m
                    throw $msg
                }
            }

            $userBytes  = [Convert]::FromBase64String($fields.username)
            $userString = [System.Text.Encoding]::UTF8.GetString($userBytes)

            return New-PSCredential -Username $userString -SecurePassword $secPassword
        }

        $legacyCredStringRegex {
            "Credential is serialized using legacy format. To avoid future complications, please reserialize the credential before storing it." | Write-Warning

            $u = $Matches.u
            $p = $Matches.p

            $ConvertArgs = @{
                String=$p
            }
        
            if ($key) {
                $keyBytes = [System.Convert]::FromBase64String($key)
                $ConvertArgs.Key = $keyBytes
            }
        
            $secPass = ConvertTo-SecureString @ConvertArgs
        
            return New-PSCredential -Username $u -SecurePassword $secPass
        }

        default {
            throw "Unrecognized credential string provided to ConvertTo-PSCredential: $CredentialString"
        }
    }
}
# END: source\credentials\ConvertTo-PSCredential.ps1


# START: source\credentials\New-PSCredential.ps1
<#
    .SYNOPSIS
    Creates a PSCredential.
 
    .DESCRIPTION
    Takes a Username and Password to create a PSCredential.
#>

function New-PSCredential{
    [CmdletBinding(DefaultParameterSetName="ClearText")]
    param(
        [parameter(Mandatory=$true, position=1)][string] $Username,
        [parameter(Mandatory=$true, position=2, ParameterSetName="ClearText")][string] $Password,
        [parameter(Mandatory=$true, position=2, ParameterSetName="SecureString")][securestring]$SecurePassword
    )

    if ($PSCmdlet.ParameterSetName -eq "ClearText") {
        $SecurePassword = ConvertTo-SecureString -String $Password -AsPlainText -Force
    }

    $cred = New-Object System.Management.Automation.PSCredential($username, $SecurePassword)

    return $cred
}
# END: source\credentials\New-PSCredential.ps1


# START: source\credentials\Restore-PSCredential.ps1
<#
.SYNOPSIS
Restores a PSCredential saved to disk using the Save-PSCredential Cmdlet.
 
.PARAMETER Path
Path to the file containing the credential.
 
.PARAMETER Key
DPAPI key to use when decrypting the credential (256 bits, base64 encoded).
#>

function Restore-PSCredential {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, HelpMessage="Path to the file from which the credential should be loaded.")]
        [String]$Path,
        [Parameter(Mandatory=$false, HelpMessage="DPAPI key to use when decrypting the credential (256 bits, base64 encoded).")]
        [string]$Key
    )

    $Path = Resolve-Path $path

    $credStr = Get-Content -Path $path -Encoding UTF8

    $convertArgs = @{
        CredentialString = $credStr
    }

    if ($Key) {
        $convertArgs.Key = $Key
    }

    return ConvertTo-PSCredential @convertArgs
}
# END: source\credentials\Restore-PSCredential.ps1


# START: source\credentials\Save-PSCredential.ps1

<#
.SYNOPSIS
Saves a PSCredential to disk as a DPAPI protected string.
 
.PARAMETER Path
Path to the file containing the credential.
 
.PARAMETER UseKey
Switch to signal that the Cmdlet should use a key when encypting the credentials.
 
.PARAMETER Key
A DPAPI key to use when encrypting the credential (256 bits, base64 encoded).
 
If this is not specified, a random 256 bit key will be generated.
#>

function Save-PSCredential{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, Position=1, HelpMessage="Path to the file where the credential should be stored.")]
        [string] $Path,
        [Parameter(Mandatory=$true, Position=2, ValueFromPipeline=$true, HelpMessage="Credential to store.")]
        [PSCredential] $Credential,
        [Parameter(Mandatory=$false, HelpMessage='Signals that the credential should be protected using a DPAPI key.')]
        [switch] $UseKey,
        [Parameter(Mandatory=$false, HelpMessage='A base64 encoded key (256 bits) to use when encrypting the credentials. If this parameter is not specified when the $UseKey switch is set, a random key will be generated.')]
        [string] $Key
    ) 

    $convertArgs = @{
        Credential = $Credential
    }

    if ($UseKey) {
        $convertArgs.UseKey = $true
        if ($PSBoundParameters.ContainsKey('Key')) {
            $convertArgs.Key = $Key
        }
    }

    $secCred = ConvertFrom-PSCredential @convertArgs

    if ($UseKey) {
        $secCred.CredentialString | Set-Content -Path $Path -Encoding UTF8
        return $secCred.Key
    } else {
        $secCred | Set-Content -Path $Path -Encoding UTF8
    }

}
# END: source\credentials\Save-PSCredential.ps1

# ACGCore.os

# START: source\os\Add-LogonOp.ps1
function Add-LogonOp{
    param(
        [string]$Name,
        [string]$Operation,
        [Switch]$RunOnce,
        [Switch]$Details
    )

    $path = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\"

    if ($PSBoundParameters.ContainsKey("RunOnce")) {
        $path += "RunOnce"
    } else {
        $path += "Run"
    }

    try {
        $value = "Powershell -WindowStyle Hidden -Command $Operation"
        $r =  New-ItemProperty -Path $path -Name $Name -Value $value -Force -ErrorAction Stop
        if ($Details) {
            return $r
        } else {
            $true
        }
    } catch {
        if ($Details) {
            return $_
        } else {
            $false
        }
    }
}
# END: source\os\Add-LogonOp.ps1


# START: source\os\Get-RegValue.ps1
# Utility to acquire registry values using reg.exe (uses Invoke-ShoutOut)
function Get-RegValue($key, $name){
    $regValueQVregex = "\s+{0}\s+(?<type>REG_[A-Z]+)\s+(?<value>.*)"
    { reg query $key /v $name } | Invoke-ShoutOut | Where-Object { 
        $_ -match ($regValueQVregex -f $name)
    } | ForEach-Object {
        $v = $Matches.value
        switch($Matches.type) {
            REG_QWORD {
                $i64c = New-Object System.ComponentModel.Int64Converter
                $v = $i64c.ConvertFrom($v)
            }
            REG_DWORD {
                $i32c = New-Object System.ComponentModel.Int32Converter
                $v = $i32c.ConvertFrom($v)
            }
        }

        $v
    }
}
# END: source\os\Get-RegValue.ps1


# START: source\os\Remove-LogonOp.ps1
function Remove-LogonOp {
    param(
        [string]$name,
        [Switch]$RunOnce,
        [Switch]$Details
    )

    $path = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\"

    if ($PSBoundParameters.ContainsKey("RunOnce")) {
        $path += "RunOnce"
    } else {
        $path += "Run"
    }

    try {
        Remove-ItemProperty -Path $path -Name $name -Force -ErrorAction Stop | Out-Null
        return $true
    } catch {
        if ($Details) {
            return $_
        } else {
            return $false
        }
    }
}
# END: source\os\Remove-LogonOp.ps1


# START: source\os\Set-ProcessPrivilege.ps1
# Found as part of a script at:
# https://social.technet.microsoft.com/Forums/windowsserver/en-US/e718a560-2908-4b91-ad42-d392e7f8f1ad/take-ownership-of-a-registry-key-and-change-permissions?forum=winserverpowershell
# and cleaned up, to be more presentable.


if (! (Get-TypeData -TypeName "ProcessPrivilegeAdjustor") ) {
    Add-Type -Path "$PSScriptRoot\.assets\ProcessPrivilegeAdjustor.cs"
}

function Set-ProcessPrivilege {
    param(
        ## The privilege to adjust. This set is taken from
        ## http://msdn.microsoft.com/en-us/library/bb530716(VS.85).aspx
        [ValidateSet(
        "SeAssignPrimaryTokenPrivilege", "SeAuditPrivilege", "SeBackupPrivilege",
        "SeChangeNotifyPrivilege", "SeCreateGlobalPrivilege", "SeCreatePagefilePrivilege",
        "SeCreatePermanentPrivilege", "SeCreateSymbolicLinkPrivilege", "SeCreateTokenPrivilege",
        "SeDebugPrivilege", "SeEnableDelegationPrivilege", "SeImpersonatePrivilege", "SeIncreaseBasePriorityPrivilege",
        "SeIncreaseQuotaPrivilege", "SeIncreaseWorkingSetPrivilege", "SeLoadDriverPrivilege",
        "SeLockMemoryPrivilege", "SeMachineAccountPrivilege", "SeManageVolumePrivilege",
        "SeProfileSingleProcessPrivilege", "SeRelabelPrivilege", "SeRemoteShutdownPrivilege",
        "SeRestorePrivilege", "SeSecurityPrivilege", "SeShutdownPrivilege", "SeSyncAgentPrivilege",
        "SeSystemEnvironmentPrivilege", "SeSystemProfilePrivilege", "SeSystemtimePrivilege",
        "SeTakeOwnershipPrivilege", "SeTcbPrivilege", "SeTimeZonePrivilege", "SeTrustedCredManAccessPrivilege",
        "SeUndockPrivilege", "SeUnsolicitedInputPrivilege")]
        $Privilege,
        ## The process on which to adjust the privilege. Defaults to the current process.
        $ProcessId = $pid,
        ## Switch to disable the privilege, rather than enable it.
        [Switch] $Disable
    )

    $processHandle = (Get-Process -id $ProcessId).Handle
    [ProcessPrivilegeAdjustor]::SetPrivilege($processHandle, $Privilege, $Disable)
}
# END: source\os\Set-ProcessPrivilege.ps1


# START: source\os\Set-RegKeyOwner.ps1
<#
.SYNOPSIS
    Grants ownership of the given registry key to the designated user (default is the current user).
.PARAMETER RegKey
    The registry key to steal, can be specified with or without a root key (HKLM, HKCU, HKU, etc.).
    if no root key is specified then the key is presumed to be under HKLM.
 
    Root keys can be designated in their short form (e.g. HKLM, HKCU) or their full-length
    form (e.g. HKEY_LOCAL_MACHINE, HKEY_CURRENT_USER).
     
    Separating the root key by a colon (:) is optional. Both "HKLM\" and "HKLM:\" are valid
    ways of designating the HKEY_LOCAL_MACHINE root key.
.PARAMETER User
    The name of the user that should become the owner of the given registry key.
#>

function Set-RegKeyOwner {
    param(
        [parameter(Mandatory=$true,  Position=1)][String]$RegKey,
        [parameter(Mandatory=$false, position=2)][String]$User=[System.Security.Principal.WindowsIdentity]::GetCurrent().Name
    )

    Set-ProcessPrivilege SeTakeOwnershipPrivilege

    $OriginalRegKey = $RegKey 
    $registry = $null

    switch -regex ($RegKey) {
        "^(HKEY_LOCAL_MACHINE|HKLM)(:)?[\\/]" {
            $registry = [Microsoft.Win32.Registry]::LocalMachine
            $RegKey = $RegKey -replace "^[^\\/]+[\\/]",""
        }
        "^(HKEY_CURRENT_USER|HKCU)(:)?[\\/]" {
            $registry = [Microsoft.Win32.Registry]::CurrentUser
            $RegKey = $RegKey -replace "^[^\\/]+[\\/]",""
        }
        "^(HKEY_USERS|HKU)(:)?[\\/]" {
            $registry = [Microsoft.Win32.Registry]::Users
            $RegKey = $RegKey -replace "^[^\\/]+[\\/]",""
        }
        "^(HKEY_CURRENT_CONFIG|HKCC)(:)?[\\/]" {
            $registry = [Microsoft.Win32.Registry]::Users
            $RegKey = $RegKey -replace "^[^\\/]+[\\/]",""
        }
        "^(HKEY_CLASSES_ROOT|HKCR)(:)?[\\/]" {
            $registry = [Microsoft.Win32.Registry]::Users
            $RegKey = $RegKey -replace "^[^\\/]+[\\/]",""
        }
        default {
            $registry = [Microsoft.Win32.Registry]::LocalMachine
        }
    }

    $key = { $registry.OpenSubKey(
        $RegKey,
        [Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree,
        [System.Security.AccessControl.RegistryRights]::takeownership
    ) } | Invoke-ShoutOut

    if (!$key) {
        shoutOut "Unable to find '$OriginalRegKey'" Red
        return 
    }

    # You must get a blank acl for the key b/c you do not currently have access
    $acl = { $key.GetAccessControl([System.Security.AccessControl.AccessControlSections]::None) } | Invoke-ShoutOut
    $me = [System.Security.Principal.NTAccount]$user
    $acl.SetOwner($me)
    { $key.SetAccessControl($acl) } | Invoke-ShoutOut | Out-Null

    # After you have set owner you need to get the acl with the perms so you can modify it.
    $acl = { $key.GetAccessControl() } | Invoke-ShoutOut
    $rule = New-Object System.Security.AccessControl.RegistryAccessRule ("BuiltIn\Administrators","FullControl","Allow")
    { $acl.SetAccessRule($rule) } | Invoke-ShoutOut | Out-Null
    { $key.SetAccessControl($acl) } | Invoke-ShoutOut | Out-Null

    $key.Close()
    shoutOut "Done!" Green
}
# END: source\os\Set-RegKeyOwner.ps1


# START: source\os\Set-RegValue.ps1
function Set-RegValue($key, $name, $value, $type=$null) {
    if (!$type) {
        if ($value -is [int16] -or $value -is [int32]) {
            $type = "REG_DWORD"
        } elseif ($value -is [int64]) {
            $type = "REG_QWORD"
        } else {
            $type = "REG_SZ"
        }
    }
    switch($type) {
        "REG_SZ" {
            { reg add $key /f /v $name /t $type /d "$value" } | Invoke-ShoutOut
        }
        default {
            { reg add $key /f /v $name /t $type /d $value } | Invoke-ShoutOut
        }
    }
}
# END: source\os\Set-RegValue.ps1


# START: source\os\Set-WinAutoLogon.ps1
#Set-WinAutoLogon.ps1

function Set-WinAutoLogon {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, Position=1, ParameterSetName="Credential")]
        [pscredential]$LogonCredential,
        [Parameter(Mandatory=$true, Position=1, ParameterSetName="Params")]
        [String]$Username,
        [Parameter(Mandatory=$true, Position=2, ParameterSetName="Params")]
        [SecureString]$Password,
        [Parameter(Mandatory=$false, Position=3, ParameterSetName="Params")]
        [String]$Domain=".",
        [Parameter(Mandatory=$false)]
        [int]$AutoLogonLimit=100000
    )

    $templatePath = "$PSScriptRoot\.assets\templates\winlogon.tmplt.reg"
    $Values = $null

    switch ($PSCmdlet.ParameterSetName) {

        "Params" {
            $values = @{
                Username    = $Username
                Password    = Unlock-SecureString $Password
                Domain      = $Domain
            }
        }

        "Credential" {
            $values = @{
                Domain      = "."
                Password    = Unlock-SecureString $LogonCredential.Password
            }
            $LogonCredential.UserName -match "((?<domain>.+)\\)?(?<username>.+)"
            if ($matches.domain) {
                $v.domain = $matches.domain
            }
            $v.Username = $matches.Username
        }

    }
    
    $values.AutoLogonLimit = $AutoLogonLimit

    $tmpFile = [System.IO.Path]::GetTempFileName()

    Format-Template -TemplatePath $templatePath -Values $values > $tmpFile

    reg import $tmpFile
    Remove-Item $tmpFile
}
# END: source\os\Set-WinAutoLogon.ps1

# ACGCore.polyfills

# START: source\polyfills\Get-ItemPropertyValue.ps1
# Polyfill to ensure Get-ItemPropertyValue is available on older OS.
if (!(Get-Command "Get-ItemPropertyValue" -ErrorAction SilentlyContinue )) {
    function Get-ItemPropertyValue {
        [CmdletBinding()]
        param(
            [paramater(
                Position=0,
                ParameterSetName="Path",
                Mandatory=$false,
                ValueFromPipeLine=$true,
                ValueFromPipelineByPropertyName=$true,
                ValueFromRemainingArguments=$false,
                DontShow=$false
            )]
            [ValidateNotNullOrEmpty()]
            [string[]]$Path,
            [paramater(
                ParameterSetName="LiteralPath",
                Mandatory=$true,
                ValueFromPipeLine=$false,
                ValueFromPipelineByPropertyName=$true,
                ValueFromRemainingArguments=$false,
                DontShow=$false
            )]
            [Alias('PSPath')]
            [string[]]$LiteralPath,
            [paramater(
                Position=1,
                Mandatory=$true,
                ValueFromPipeLine=$false,
                ValueFromPipelineByPropertyName=$false,
                ValueFromRemainingArguments=$false,
                DontShow=$false
            )]
            [Alias('PSProperty')]
            [string[]]$Name,
            [paramater(
                Mandatory=$false,
                ValueFromPipeLine=$false,
                ValueFromPipelineByPropertyName=$false,
                ValueFromRemainingArguments=$false,
                DontShow=$false
            )]
            [string]$Filter,
            [paramater(
                Mandatory=$false,
                ValueFromPipeLine=$false,
                ValueFromPipelineByPropertyName=$false,
                ValueFromRemainingArguments=$false,
                DontShow=$false
            )]
            [string[]]$Include,
            [paramater(
                Mandatory=$false,
                ValueFromPipeLine=$false,
                ValueFromPipelineByPropertyName=$false,
                ValueFromRemainingArguments=$false,
                DontShow=$false
            )]
            [string[]]$Exclude,
            [paramater(
                Mandatory=$false,
                ValueFromPipeLine=$false,
                ValueFromPipelineByPropertyName=$true,
                ValueFromRemainingArguments=$false,
                DontShow=$false
            )]
            [Credential()]
            [System.Management.Automation.PSCredential]$Credential
        )

        $params = $MyInvocation.Boundparameters

        $r = Get-ItemProperty @params
        

        foreach($n in $Name) {
            $r.$n
        }
        
    }
}
# END: source\polyfills\Get-ItemPropertyValue.ps1

# ACGCore.securestrings

# START: source\securestrings\ConvertFrom-CertificateSecuredString.ps1
<#
.SYNOPSIS
Decryps a certificate-secured string and turns it into a SecureString.
 
.PARAMETER CertificateSecuredString
Certificate-secured string to convert into a SecureString.
 
.PARAMETER Certificate
Certificate with an associated private key should be used to decrypt the secured string.
 
.PARAMETER Thumbprint
Thumbprint of the certificate that should be used to decrypt the secured string.
 
#>

function ConvertFrom-CertificateSecuredString {
    param(
        [parameter(Mandatory=$true, ValueFromPipeline=$true, HelpMessage="Base64-encoded certificate-encrypted string to decrypt.")]
        [ValidatePattern('[a-z0-9+\/=]+')]
        [string]$CertificateSecuredString,
        [parameter(Mandatory=$true, ParameterSetName="Certificate")]
        [System.Security.Cryptography.X509Certificates.X509Certificate]$Certificate,
        [parameter(Mandatory=$true, ParameterSetName="Thumbprint")]
        [string]$Thumbprint
    )

    $privateKey = $null

    switch ($PSCmdlet.ParameterSetName) {

        Certificate {
            # Verify that the certificate has a private key:
            if (-not $Certificate.HasPrivateKey) {
                $msg = "Unable to decrypt string. No private key available for the certificate used to encrypt the credential (thumbprint '{0}')." -f $Thumbprint
                throw $msg
            }

            # Check if the private key is included in the certificate object:
            if ($null -ne $Certificate.privateKey) {
                $privateKey = $Certificate.privateKey.Key
                break
            }

            # Check if we can find the associated private key:
            try {
                $privateKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate)
            } catch {
                $msg = "Failed to find a private key for the provided certificate: SN={0},{1} (Thumbprint: '{0}')" -f $Certificate.SerialNumber, $Certificate.Issuer, $Certificate.Thumbprint
                $ex = New-Object $msg $_.Exception
                throw $ex
            }
        }

        Thumbprint {
            # Retrieve the certificate:
            $cert = $null
            try {
                $cert = Get-ChildItem -Path 'Cert:\' -Recurse -ErrorAction Stop | Where-Object Thumbprint -eq $Thumbprint
            } catch {
                $msg = "Unexpected error while looking up the certificate (thumbprint '{0}')." -f $Thumbprint
                $ex = New-Object $msg $_
                throw $ex
            }

            # Verify that we found a certificate:
            if ($null -eq $cert) {
                $msg = "Failed to find the certificate (thumbprint '{0}')." -f $Thumbprint
                throw $msg
            }

            # Verify that we retrieved only a single certificate:
            if ($cert -is [array]) {
                # Eliminate any certificat that does not have an associated private key:
                $cert = $cert | Where-Object HasPrivateKey

                # Verify that we still have at least 1:
                if ($null -eq $cert) {
                    $msg = "No certificate with associated private key available for the thumbprint ('{0}')" -f $Thumbprint
                    throw $msg
                }

                # More than 1 certificate found.
                # This should not pretty much never happen, unless the store contains duplicates of the same certificate.
                # Verify that they are the same certificate:
                $cert = $cert | Sort-Object { "Cert={1}, {0}" -f $_.Issuer, $_.SerialNumber } -Unique
                if ($cert -is [array]) {
                    $msg = "More than 1 certificate found for the thumbprint ('{0}')." -f $Thumbprint
                    throw $msg
                }
            }

            # Verify that the certificate has an associated private key:
            if (-not $cert.HasPrivateKey) {
                $msg = "Unable to decrypt string. No private key available for the certificate used to encrypt the credential (thumbprint '{0}')." -f $Thumbprint
                throw $msg
            }

            $privateKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
        }

        default {
            $msg = "Unknown ParameterSet ('{0}'). This shouldn't happen, but indicates that someone has pushed buggy/incomplete code." -f $PSCmdlet.ParameterSetName
            throw $msg
        }
    }

    if ($null -eq $privateKey) {
        $msg = "Failed to retrieve the private key for the specified certificate (thumbprint '{0}')." -f $Thumbprint
        throw $msg
    }

    $encBytes   = [convert]::FromBase64String($CertificateSecuredString)
    $plainBytes      = $privateKey.Decrypt($encBytes, [System.Security.Cryptography.RSAEncryptionPadding]::Pkcs1)
    $plainString     = [System.Text.Encoding]::Default.GetString($plainBytes)
    Remove-Variable 'plainBytes'
    $secString  = ConvertTo-SecureString -String $plainString -AsPlainText -Force
    Remove-Variable 'plainString'

    return $secString
}
# END: source\securestrings\ConvertFrom-CertificateSecuredString.ps1


# START: source\securestrings\ConvertTo-CertificateSecuredString.ps1

<#
.SYNOPSIS
Transforms a SecureString to a certificate-secured string.
 
.PARAMETER SecureString
The SecureString to convert.
 
.PARAMETER Certificate
A X509 certificate object that should be used to encrypt the string.
 
.PARAMETER Thumbprint
The thumbprint of a certificate to use when encrypting the string.
 
The Cmdlet will look in the entire store for a certificate with the given thumbprint.
 
If more than 1 certificate is found with the thumbprint, the Cmdlet will verify that they
are in fact duplicate copies of the same certificate by check that they have the same Issuer
and Serial Number.
 
If more than 1 certificate are found to have the same thumbprint this Cmdlet will throw
an exception.
 
WARNING: You will need to use the private key associated with the certificate to
decrypt the string. This Cmdlet does not check if the private key is available.
 
.PARAMETER CertificateFilePath
Path to an existing file containing a DER-encoded certificate to use when encoding the string.
#>

function ConvertTo-CertificateSecuredString {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, Position=1, ValueFromPipeline=$true)]
        [securestring]$SecureString,
        [parameter(Mandatory=$true, Position=2, ParameterSetName="Certificate")]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,
        [parameter(Mandatory=$true, Position=2, ParameterSetName="Thumbprint")]
        [string]$Thumbprint,
        [Parameter(Mandatory=$true, ParameterSetName="CertificateFilePath")]
        [string]$CertificateFilePath
    )

    $pubKey = switch ($PSCmdlet.ParameterSetName) {

        Certificate {
            $Certificate.publicKey.Key
        }

        Thumbprint {
            # Retrieve the certificate:
            $cert = $null
            try {
                $cert = Get-ChildItem -Path 'Cert:\' -Recurse -ErrorAction Stop | Where-Object Thumbprint -eq $Thumbprint
            } catch {
                $msg = "Unexpected error while looking up the certificate ('{0}')." -f $Thumbprint
                $ex = New-Object $msg $_
                throw $ex
            }

            # Verify that we found a certificate:
            if ($null -eq $cert) {
                $msg = "Failed to find the certificate ('{0}')." -f $Thumbprint
                throw $msg
            }

            # Verify that we retrieved only a single certificate:
            if ($cert -is [array]) {
                # More than 1 certificate found.
                # This should not pretty much never happen, unless the store contains duplicates of the same certificate.
                # Verify that they are the same certificate:
                $cert = $cert | Sort-Object { "Cert={1}, {0}" -f $_.Issuer, $_.SerialNumber } -Unique
                if ($cert -is [array]) {
                    $msg = "More than 1 certificate found for the thumbprint ('{0}')." -f $Thumbprint
                    throw $msg
                }
            }

            $cert.PublicKey.Key
        }

        CertificateFilePath {
            Try {
                $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $CertificateFilePath -ErrorAction Stop
            } catch {
                $msg = "Failed to load the specified certificate file ('{0}')." -f $CertificateFilePath
                $ex = New-Object System.Exception $msg $_.Exception
                throw $ex
            }

            $cert.PublicKey.Key
        }
    }

    # Unlock the securestring and turn it into a byte array:
    $plain = Unlock-SecureString -SecString $SecureString
    $plainBytes = [System.Text.Encoding]::Default.GetBytes($plain)
    Remove-Variable 'plain'
    # Use the public key to encrypt the byte array:
    $encBytes = $pubKey.encrypt($plainBytes, [System.Security.Cryptography.RSAEncryptionPadding]::Pkcs1)
    Remove-Variable 'plainBytes'
    # Convert the encrypted byte array to Bas64 string:
    $encString = [convert]::ToBase64String($encBytes)

    return $encString
}
# END: source\securestrings\ConvertTo-CertificateSecuredString.ps1


# START: source\securestrings\Export-SecureString.ps1
<#
.SYNOPSIS
Takes a protected SecureString and exports it to a portable format as an encrypted string (can also exort as a plaintext string).
 
#>

function Export-SecureString {
    [CmdletBinding(DefaultParameterSetName="dpapi")]
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, Position=1)]
        [Alias('SecString')]
        [ValidateNotNull()]
        [securestring]$SecureString,
        [Parameter(Mandatory=$true, ParameterSetName='dpapi.key', HelpMessage='Signals that the credential should be protected using a DPAPI key.')]
        [Alias('UseKey')]
        [switch] $UseDPAPIKey,
        [Parameter(Mandatory=$false, ParameterSetName='dpapi.key', HelpMessage='A base64 encoded key (128, 192 or 256 bits) to use when encrypting the string. If this parameter is not specified when the $UseKey switch is set, a random 256 bit key will be generated.')]
        [ValidateScript({
            # Verify that this is a valid Base64 string:
            $base64Pattern = "^[a-z0-9+\/\r\n]+={0,2}$"
            if ($_.Length % 4 -ne 0) {
                $msg = "Invalid base64 string provided as Key: string length should be evenly divisble by 4, current string length mod 4 is {0} ('{1}')" -f ($_.Length % 4), $_
                throw $msg
            }
            if ($_ -notmatch $base64Pattern) {
                $msg = "Invalid base64 string provided as Key: '{0}' contains invalid charactes." -f $_
                throw $msg
            }
            return $true
        })]
        [Alias('Key')]
        [string] $DPAPIKey,
        [Parameter(Mandatory=$true, ParameterSetName='x509.managed', HelpMessage='Thumbprint of the certificate that should be used to encrypt the resulting string. Warning: you will need the corresponding private key to decrypt the string.')]
        [string] $Thumbprint,
        [Parameter(Mandatory=$true, ParameterSetName='plain', HelpMessage='Disable encryption, causing the plain text be base64 encoded.')]
        [switch] $NoEncryption
    )

    switch ($PSCmdlet.ParameterSetName) {
        dpapi {
            return ConvertFrom-SecureString -SecureString $SecureString
        }

        dpapi.key {

            $convertArgs = @{
                SecureString = $SecureString
            }

            if ($PSBoundParameters.ContainsKey('Key')) {
                $bytes = [System.Convert]::FromBase64String($Key)
                if ($bytes.count -notin 16, 24, 32) {
                    $msg = "Invalid key provided for SecureString export: expected a Base64 string convertable to a 16, 24 or 32 byte array (found a string convertible to {0} bytes)." -f $bytes.Count
                    throw $msg
                }
            } else {
                $r = $script:__RNG
                $bytes = for($i = 0; $i -lt 32; $i++) { $r.next(0, 256) }
            }
            $convertArgs.Key = $bytes

            return @{
                String = ConvertFrom-SecureString @ConvertArgs
                Key    = [System.Convert]::ToBase64String($convertArgs.Key)
            }

        }

        x509.managed {
            return convertTo-CertificateSecuredString -SecureString $SecureString -Thumbprint $Thumbprint
        }

        plain {
            $Marshal = [Runtime.InteropServices.Marshal]
            $bstr = $Marshal::SecureStringToBSTR($SecureString)
            $r = $Marshal::ptrToStringAuto($bstr)
            $Marshal::ZeroFreeBSTR($bstr)
            return $r
        }
    }
}
# END: source\securestrings\Export-SecureString.ps1


# START: source\securestrings\Import-SecureString.ps1
<#
.SYNOPSIS
Imports a provided string into the current context as a SecureString.
 
.PARAMETER DPAPIKey
A Base64-encoded string corresponding to a 128, 192 or 256 bit key that should be used to decrypt the string.
 
.PARAMETER Thumbprint
Thumbprint of a certificate to use when decrypting the string.
 
The cmdlet llooks in the entire certificate store.
 
If no certificate matching the thumbprint can be found, or if none of the found certificates have an
associated private key, the Cmdlet will throw an exception.
 
.PARAMETER NoEncryption
Indicates that the string is not encrypted an should be imported as-is.
 
This is functionally the same as writing (for a given string $s):
    ConvertTo-SecureString -String $s -AsPlaintext -Force
#>

Function Import-SecureString {
    [CmdletBinding(DefaultParameterSetName='dpapi')]
    param(
        [Parameter(Mandatory=$true, Position=1, ValueFromPipeline=$true, HelpMessage="The exported SecureString to import")]
        [string]$String,
        [Parameter(Mandatory=$false, ParameterSetName='dpapi.key', HelpMessage='A base64 encoded key (128, 192 or 256 bits) to use when encrypting the string. If this parameter is not specified when the $UseKey switch is set, a random 256 bit key will be generated.')]
        [ValidateScript({
            # Verify that this is a valid Base64 string:
            $base64Pattern = "^[a-z0-9+\/\r\n]+={0,2}$"
            if ($_.Length % 4 -ne 0) {
                $msg = "Invalid base64 string provided as Key: string length should be evenly divisble by 4, current string length mod 4 is {0} ('{1}')" -f ($_.Length % 4), $_
                throw $msg
            }
            if ($_ -notmatch $base64Pattern) {
                $msg = "Invalid base64 string provided as Key: '{0}' contains invalid charactes." -f $_
                throw $msg
            }
            return $true
        })]
        [Alias('Key')]
        [string] $DPAPIKey,
        [Parameter(Mandatory=$true, ParameterSetName='x509.managed', HelpMessage='Thumbprint of the certificate that should be used to encrypt the resulting string. Warning: you will need the corresponding private key to decrypt the string.')]
        [string] $Thumbprint,
        [Parameter(Mandatory=$true, ParameterSetName='plain', HelpMessage='Disable encryption, causing the plain text be base64 encoded.')]
        [switch] $NoEncryption
    )

    switch ($PSCmdlet.ParameterSetName) {
        dpapi {
            return ConvertTo-SecureString -String $String
        }

        dpapi.key {
            $keyBytes = [convert]::FromBase64String($DPAPIKey)
            return ConvertTo-SecureString -String $string -Key $keyBytes
        }

        x509.managed {
            try {
                return ConvertFrom-CertificateSecuredString -CertificateSecuredString $String -Thumbprint $Thumbprint
            } catch {
                $msg = "Failed to decrypt the string using certificat (Thumbprint: {0}). See inner exception for details." -f $header.t
                $ex = New-Object System.Exception $msg, $_.Exception
                throw $ex
            }
        }

        plain {
            return ConvertTo-SecureString -String $String -AsPlainText -Force
        }
    }
}
# END: source\securestrings\Import-SecureString.ps1


# START: source\securestrings\Unlock-SecureString.ps1
<#
.SYNOPSIS
Transforms a SecureString back into a plain string. Must the run by the same user, on the same computer where it was produced.
 
.DESCRIPTION
Transforms a SecureString back into a plain string. Must the run by the same user, on the same computer where it was produced.
 
This is a wrapper for Export-SecureString, and is equivalent to:
    Export-SecureString -SecureString $SecureString -NoEncryption
 
.PARAMETER SecureString
SecureString to unlock.
#>

function Unlock-SecureString {
    param(
        [Parameter(Mandatory=$true, Position=1, ValueFromPipeline=$true)]
        [ValidateNotNull()]
        [Alias('SecString')] # Backwards compatibility for pre version 0.10.0.
        [SecureString]$SecureString
    )
    
    return Export-SecureString -SecureString $SecureString -NoEncryption
}
# END: source\securestrings\Unlock-SecureString.ps1

# ACGCore.strings

# START: source\strings\ConvertFrom-Base64String.ps1
<#
.DESCRIPTION
Converts the provided Base64 encoded string to a regular string.
#>

function ConvertFrom-Base64String {
    [CmdletBinding(DefaultParameterSetName="Encoding")]
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, Position=1, HelpMessage="Base64-encoded string to convert.")]
        [ValidatePattern('^([a-z0-9+\/]+={0,2})?$')]
        [ValidateScript({
            if ($_.Length % 4 -ne 0) {
                $msg = "Invalid string length. Base64-encoded strings should have a length evently divisible by 4, found {0} ('{1}')" -f $_.Length, $_
                throw $msg
            }
            return $true
        })]
        [string]$Base64String,
        [Parameter(Mandatory=$false, Position=2, HelpMessage="Encoding to convert the string into.")]
        [System.Text.Encoding]$OutputEncoding = [System.Text.Encoding]::default,
        [Parameter(Mandatory=$true, ParameterSetName="Raw", HelpMessage="Disable output encoding, returns a byte array.")]
        [switch]$NoEncoding
    )

    process {
        foreach($s in $Base64String) {
            if ($s.Length % 4 -ne 0) {
                Throw
            }

            $bytes = [convert]::FromBase64String($Base64String)

            if ($PSCmdlet.ParameterSetName -eq "Raw") {
                $bytes
            } else {
                $OutputEncoding.GetString($bytes)
            }
        }
    }
}
# END: source\strings\ConvertFrom-Base64String.ps1


# START: source\strings\ConvertFrom-UnicodeEscapedString.ps1
#ConvertFrom-UnicodeEscapedString.ps1

function ConvertFrom-UnicodeEscapedString {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [string]$InString
    )

    return [System.Text.RegularExpressions.Regex]::Unescape($InString)
}
# END: source\strings\ConvertFrom-UnicodeEscapedString.ps1


# START: source\strings\ConvertTo-Base64String.ps1
<#
.DESCRIPTION
Converts the provided string to a Base64-encoded string.
#>

function ConvertTo-Base64String {
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, Position=1, HelpMessage="String to convert to Base64.")]
        [ValidateNotNull()]
        [string[]]$String,
        [Parameter(Mandatory=$false, Position=2, HelpMessage="The encoding of the string to convert.")]
        [System.Text.Encoding]$InputEncoding = [System.Text.Encoding]::Default

    )

    process {
        foreach ($s in $String) {
            if ($String -eq '') {
                ''
            } else {
                $bytes = $InputEncoding.GetBytes($String)
                [convert]::ToBase64String($bytes)
            }
        }
    }
}
# END: source\strings\ConvertTo-Base64String.ps1


# START: source\strings\ConvertTo-UnicodeEscapedString.ps1
#ConvertTo-UnicodeEscapedString.ps1

function ConvertTo-UnicodeEscapedString {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [String]$inString
    )

    $sb = New-Object System.Text.StringBuilder

    $inChars = [char[]]$inString

    foreach ($c in $inChars) {
        $encV = if ($c -gt 127) {
            "\u"+([int]$c).ToString("X4")
        } else {
            $c
        }
        $sb.Append($encV) | Out-Null
    }

    return $sb.ToString()
}
# END: source\strings\ConvertTo-UnicodeEscapedString.ps1


# START: source\strings\New-RandomString.ps1
<#
.SYNOPSIS
Generates a new random string of a given length using the given pool of candidate characters.
 
.PARAMETER Length
Number of characters in the string.
 
Minimum length is 0. Default is 8.
 
.PARAMETER Characters
String of candidate characters that will be used in the generated string.
 
If a character appears more than once, it will be most likely to appear in the
generated string.
 
Minimum length is 1.
 
This defaults to "abcdefghijklmnopqrstuvwxyz0123456789-_".
 
.PARAMETER CharacterSet
Alternatively to specifying a string of candidate Characters, use a predefined set:
    - Binary: 0 and 1
    - Base8: Numbers 0-7
    - Base16: Numbers 0-9 and characters a-f
    - Alphanumeric: All characters from the english alphabet (a-z) and numbers 0-9.
    - Base64: All characters a-z, numbers 0-9 and the symbols '+', '/' ('=' is excluded because it is used for padding and MUST appear at the end of the string).
 
.PARAMETER ReturnType
The type of object to return:
    - String: Just return the plain string ([string]).
    - Bytes: Converts the string into an array of bytes ([byte[]]) before returning it.
    - Base64: Converts the string into a bas64 representation before returning it.
    - SecureString: Return the string as a SeureString object ([SecureString]).
 
.PARAMETER AsSecureString
Return the generated string as a SecureString instead of a plain String.
 
#>

function New-RandomString {
    [CmdletBinding(DefaultParameterSetName="Candidates")]
    param(
        [Parameter(Mandatory=$false, HelpMessage="Length of the string to generate.")]
        [ValidateScript({if ($_ -ge 0) { return $true }; throw "Invalid Length requested ($_), cannot generate a string of negative length." })]
        [int]$Length=8,
        [Parameter(Mandatory=$false, ParameterSetName="Candidates", HelpMessage="String of candidate characters.")]
        [ValidateNotNullOrEmpty()]
        [string]$Characters="abcdefghijklmnopqrstuvwxyz0123456789-_",
        [Parameter(Mandatory=$true, ParameterSetName="CharacterSet", HelpMessage="The set of characters that the string should consist of.")]
        [ValidateSet('Binary', 'Base8', 'Base10', 'Base16', 'Alphanumeric', 'Base64')]
        [string]$CharacterSet,
        [Parameter(Mandatory=$false, HelpMessage="Format to return the string in (default 'String').")]
        [ValidateSet("String", "Base64", "Bytes", "SecureString")]
        [string]$ReturnFormat="String",
        [Parameter(Mandatory=$false, HelpMessage="Determines if selected characters will retain their original case.")]
        [bool]$RandomCase=$true,
        [Parameter(Mandatory=$false, HelpMessage="Causes the string to be returned as a SecureString. For legacy reasons, this overrides `$ReturnFormat.")]
        [switch]$AsSecureString
    )

    if ($Length -eq 0) {
        # Zero length string requested, return empty string.
        return ""
    }

    if ($PSCmdlet.ParameterSetName -eq "CharacterSet") {
        $Characters = switch ($CharacterSet) {
            Binary {
                "01"
            }
            Base8 {
                "01234567"
            }
            Base10 {
                "0123456789"
            }
            Base16 {
                "0123456789abcdef"
            }
            Alphanumeric {
                "0123456789abcdefghijklmnopqrstuvwxyz"
            }
            Base64 {
                "0123456789abcdefghijklmnopqrstuvwxyz+/"
            }
        }
    }

    $rng = $script:__RNG

    if ($AsSecureString -or ($ReturnFormat -eq "SecureString")) {
        $password = New-Object securestring
        for ($i = 0; $i -lt $Length; $i++) {
            $c = $Characters[$rng.Next($Characters.Length)]
            if ($RandomCase) {
                $c = if ($rng.Next(10) -gt 4) {
                    [char]::ToUpper($c)
                } else {
                    [char]::ToLower($c)
                }
            }

            $password.AppendChar($c)
        }
        return $password
    }

    $password = ""
    for ($i = 0; $i -lt $Length; $i++) {
        $c = $Characters[$rng.Next($Characters.Length)]
        if ($RandomCase) {
            $c = if ($rng.Next(10) -gt 4) {
                [char]::ToUpper($c)
            } else {
                [char]::ToLower($c)
            }
        }

        $password += $c
    }

    switch($ReturnFormat) {
        String {
            return $password
        }

        Bytes {
            return [System.Text.Encoding]::Default.GetBytes($password)
        }

        Base64 {
            $bytes = [System.Text.Encoding]::Default.GetBytes($password)
            return [System.Convert]::ToBase64String($bytes)
        }
    }
}
# END: source\strings\New-RandomString.ps1

# ACGCore.templates

# START: source\templates\_buildTemplateDigest.ps1
function _buildTemplateDigest{
    param($template, $StartTag, $EndTag, $templateCache)
    
    $EndTagStart = $EndTag[0]
    if ($EndTagStart -eq '\') {
        $EndTagStart.Substring(0, 2)
    }
    $EndTagRemainder = $EndTag.Substring($EndTagStart.Length)
    $InterpolationRegex = "{0}(\((?<path>.+)\)|(?<command>([^{1}]|{1}(?!{2}))+)){3}" -f $StartTag, $EndTagStart, $EndTagRemainder, $EndTag
    
    Write-Debug "Building digest..."
    $__c__ = $templateCache
    $__c__.Digest = @()
    
    $__regex__ = New-Object regex ($InterpolationRegex, [System.Text.RegularExpressions.RegexOptions]::Multiline)
    $__meta__ = @{ LastIndex = 0 }
    
    $__regex__.Replace(
        $template,
        {
            param($match)
            # Isolate information about the expression.
            $__li__ = $__meta__.LastIndex
            $__g0__ = $match.Groups[0]
            $__path__    = $match.Groups["path"]
            $__command__= $match.Groups["command"]
            
            # Collect string literal preceeding this expression and add it to the digest.
            $__ls__ = $template.Substring($__li__, ($__g0__.index - $__li__))
            $__meta__.LastIndex = $__g0__.index + $__g0__.length
            $__c__.Digest += $__ls__
            
            # Process the expression:
            if ($__command__.Success) {
                # Expression is a command: turn it into a script block and add it to the digest.
                $__c__.Digest += [scriptblock]::create($__command__.value)
            } elseif ($__path__.Success){
                # Expand any variables in the path and add the expanded path to digest:
                $p = $ExecutionContext.InvokeCommand.ExpandString($__path__.Value)
                $__c__.Digest += @{ path=$p }                        
            }
            
            $__meta__ | Out-String | Write-Debug
            
        }
    ) | Out-Null
    
    
    if ($__meta__.LastIndex -lt $template.length) {
        $__c__.Digest += $template.substring($__meta__.LastIndex)
    }
}
# END: source\templates\_buildTemplateDigest.ps1


# START: source\templates\Format-Template.ps1
<#
.SYNOPSIS
Renders a template file.
 
.DESCRIPTION
Renders a template file of any type (HTML, CSS, RDP, etc..) using powershell expressions
written between '<<' and '>>' markers to interpolate dynamic values.
 
Files may also be included into the template by using <<(<path to file>)>>, if the file is
a .ps1 file it will be interpreted as an expression to be executed, otherwise it will be
treated as a template file and rendered using the same Values.
 
.PARAMETER templatePath
The path to the template file that should be rendered (relative or fully qualified,
UNC paths not supported).
 
.PARAMETER values
A hashtable of values that should be used when resolving powershell expressions.
The keys in this hashtable will introduced as variables into the resolution context.
 
The $values variable itself is available as well.
 
.PARAMETER Cache
A hashtable used to cache the results of loading template files.
 
Passing this parameter allows you to retain the cache between calls to Format-Template,
otherwise a new hashtable will be generated for each call to Format-Template.
 
Recursive calls to Format-Template will attempt to reuse the same cache object.
 
During rendering the cache is available as '$__RenderCache'.
 
.PARAMETER StartTag
Tag used to indicate the start of a section in the text that should be interpolated.
 
This string will be treated as a regular expression, so any special characters
('*', '+', '[', ']', '(', ')', '\', '?', '{', '}', etc) should be escaped with a '\'.
 
The default start tag is '<<'.
 
 
.PARAMETER EndTag
Tag used to indicate the end of a section in the text that should be interpolated.
 
This string will be treated as a regular expression, so any special characters
('*', '+', '[', ']', '(', ')', '\', '?', '{', '}', etc) should be escaped with a '\'.
 
The default end tag is '>>'.
 
.EXAMPLE
Contents of .\page.template.html:
    <h1><<$Title>></he1>
    <h2><<$values.Chapter1>></h2>
     
    <<(.\pages\1.html)>>
 
Contents of .\pages\1.html:
 
    It was the best of times, it was the worst of times.
 
Running:
    $details = @{
        Title = "A tale of two cities"
        Chapter1 = "The Period"
    }
    Format-Template .\page.template.html $details
 
Will yield:
    <h1>A tale of two cities</h1>
    <h2>The Period</h2>
     
    It was the best of times, it was the worst of times.
 
 
.NOTES
The markup using the default '<<' and '>>' tags to denote the start and end of an interpolated
expression precludes the use of the '>>' output operator in the expressions. This is considered
acceptable, since the intention of the expressions is to introduce values into the text,
rather than writing to the disk.
 
Any expression that is so complicated that you might need to write to the disk should
probably be handled as a closure or a function passed in via the $values parameter, or
a file included using a <<()>> expression.
 
Alternatively, you can use the the EndTag parameter top provide another acceptable end tag (e.g. '!>>').
 
#>

function Format-Template{
    [CmdletBinding(DefaultParameterSetName="TemplatePath")]    
    param(
        [parameter(
            Mandatory=$true,
            Position=1,
            ParameterSetName="TemplatePath",
            HelpMessage="Path to the template file that should be rendered. Available when rendering."
        )]
        [String]$TemplatePath,
        [parameter(
            Mandatory=$false,
            ParameterSetName="TemplatePath",
            HelpMessage="Character Encoding of the template file (defaults to UTF8)."
        )]
        [Microsoft.PowerShell.Commands.FileSystemCmdletProviderEncoding]$TemplateEncoding = [Microsoft.PowerShell.Commands.FileSystemCmdletProviderEncoding]::UTF8,
        [parameter(
            Mandatory=$true,
            Position=1,
            ParameterSetName="TemplateString",
            HelpMessage="Template string to render."
        )]
        [String]$TemplateString,
        [parameter(
            Mandatory=$true,
            Position=2,
            HelpMessage="Hashtable with values used when interpolating expressions in the template. Available when rendering."
        )]
        [hashtable]$Values,
        [Parameter(
            Mandatory=$false,
            Position=3,
            HelpMessage='Optional Hashtable used to cache the content of files once they are loaded. Pass in a hashtable to retain cache between calls. Available as $__RenderCache when rendering.'
        )]
        [hashtable]$Cache = $null,
        [Parameter(
            Mandatory=$false,
            HelpMessage='Tag used to open interpolation sections. Regular Expression.'
        )]
        [string]$StartTag = $script:__InterpolationTags.Start,
        [Parameter(
            Mandatory=$false,
            HelpMessage='Tag used to close interpolation sections. Regular expression.'
        )]
        [string]$EndTag    = $script:__InterpolationTags.End
    )

    
    $script:__InterpolationTagsHistory.Push($script:__InterpolationTags)

    $script:__InterpolationTags = @{
        Start    = $StartTag
        End        = $EndTag
    }

    trap {
        $script:__InterpolationTags = $script:__InterpolationTagsHistory.Pop()
        throw $_
    }

    if ($Cache) {
        Write-Debug "Cache provided by caller, updating global."
        $script:__RenderCache = $Cache
    }

    if ($null -eq $Cache) { 
        
        Write-Debug "Looking for cache..."
        
        if ($Cache = $script:__RenderCache) {
            Write-Debug "Using global cache."
        } elseif ($cacheVar = $PSCmdlet.SessionState.PSVariable.Get("__RenderCache")) {
            # This is a recursive call, we can reuse the cache from parent.
            $Cache = $cacheVar.Value
            Write-Debug "Found cache in parent context."
        }

    }

    if ($null -eq $cache) {
        Write-Debug "Failed to get cache from parent. Creating new cache."
        $Cache = @{}
        $script:__RenderCache = $Cache
    }

    # Getting template string:
    $template = $null
    switch ($PSCmdlet.ParameterSetName) {
        TemplatePath {
            # Loading template from file, and adding it to cache:
            $templatePath = Resolve-Path $templatePath

            Write-Debug "Path resolved to '$templatePath'"

            if ($Cache.ContainsKey($templatePath)) {
                Write-Debug "Found path in cache..."
                try {
                    $item = Get-Item $TemplatePath
                    if ($item.LastWriteTime.Ticks -gt $Cache[$templatePath].LoadTime.Ticks) {
                        Write-Debug "Cache is out-of-date, reloading..."
                        $t = Get-Content -Path $templatePath -Raw -Encoding $TemplateEncoding
                        $Cache[$templatePath] = @{ Value = $t; LoadTime = [datetime]::now }
                    }
                } catch { <# Do nothing for now #> }
                $template = $Cache[$templatePath].Value
            } else {
                Write-Debug "Not in cache, loading..."
                $template = Get-Content -Path $templatePath -Raw -Encoding $TemplateEncoding
                $Cache[$templatePath] = @{ Value = $template; LoadTime = [datetime]::now }
            }
        }

        TemplateString {
            $template = $TemplateString
        }
    }

    # Move Cache out of the of possible user-space values.
    $__RenderCache = $Cache
    Remove-Variable "Cache"

    # Defining TemplateDir here to make it accessible when evaluating scriptblocks.
    $TemplateDir = switch ($PSCmdlet.ParameterSetName) {
        TemplatePath {
            $templatePath | Split-Path -Parent
        }
        TemplateString {
            # Using a template string, so use current working directory:
            $pwd.Path
        }
    }
    
    # Get the digest of the template string:
    $__digest__ = switch ($PSCmdlet.ParameterSetName) {
        TemplatePath {
            # Using a template file, check if we already have a digest in the cache:
            if (!$__RenderCache[$templatePath].ContainsKey("Digest")) {
                _buildTemplateDigest $template $StartTag $EndTag $__RenderCache[$templatePath]
            }

            $__RenderCache[$templatePath].Digest
        }
        TemplateString {
            # Using a template string, don't add it to the cache:
            $c = @{}
            _buildTemplateDigest $template $StartTag $EndTag $c
            $c.Digest
        }
    }
    
    # Expand values into user-space to make them more accessible during render.
    $values.GetEnumerator() | ForEach-Object {
        New-Variable $_.Name $_.Value
    }
    
    Write-Debug "Starting Render..."
    $__parts__ = $__digest__ | ForEach-Object {
        $__part__ = $_
        switch ($__part__.GetType()) {
            "hashtable" {
                if ($__part__.path) {
                    Write-Debug "Including path..." 
                    $__c__ = Format-Template -TemplatePath $__part__.path -Values $Values

                    if ($__part__.path -like "*.ps1") {
                        $__s__ = [scriptblock]::create($__c__)
                        try {
                            $__s__.Invoke()
                        } catch {
                            $msg = "An unexpected exception occurred while Invoking '{0}'." -f $__part__.path
                            $e = New-Object System.Exception $msg, $_.Exception

                            throw $e
                        }
                    } else {
                        $__c__
                    }
                }
            }

            "scriptblock" {
                try {
                    $__part__.invoke()
                } catch {
                    $msg = "An unexpected exception occurred while rendering an expression: '{0}'." -f $__part__
                    $e = New-Object System.Exception $msg, $_.Exception

                    throw $e
                }
            }

            default {
                $__part__
            }
        }
    }

    $script:__InterpolationTags = $script:__InterpolationTagsHistory.Pop()
    
    $__parts__ -join ""

    
}
# END: source\templates\Format-Template.ps1