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 (Test-Path -Path $Path -PathType Leaf) ) { $lines = Get-Content -Path $Path -Encoding UTF8 } else { . $handleError -Message "<InvalidPath>The given path doesn't lead to an existing file: '$Path'" return } $currentDir = Split-Path -Parent $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 } Set-Content -Path $Path -Value $output -Encoding [System.Text.Encoding]::UTF8 } 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-EnvironmentPath.ps1 function Add-EnvironmentPath { [CmdletBinding()] param( [parameter(Mandatory=$true, Position=1, HelpMessage="The path to add to the Path environmental variable.")] [string]$Path, [parameter(Mandatory=$false, Position=2, HelpMessage="The type of environment variable to target.")] [System.EnvironmentVariableTarget]$Target = [System.EnvironmentVariableTarget]::Process ) $oldPath = [System.Environment]::GetEnvironmentVariable('Path', $Target) $paths = if ($null -eq $oldPath) { [string[]]@() } else { [string[]]$oldPath.split(';') } if ($paths -contains $Path) { return } $newPath = ($paths + $Path) -join ";" [System.Environment]::SetEnvironmentVariable('Path', $newPath, $Target) } # END: source\os\Add-EnvironmentPath.ps1 # 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-EnvironmentPaths.ps1 function Get-EnvironmentPaths() { [CmdletBinding()] param( [parameter(Mandatory=$false, Position=1, HelpMessage="The type of environment variable to retrieve.")] [System.EnvironmentVariableTarget]$Target = [System.EnvironmentVariableTarget]::Process ) $v = [System.Environment]::GetEnvironmentVariable('Path', $Target) if ($null -ne $v) { return $v.split(';') } else { return $null } } # END: source\os\Get-EnvironmentPaths.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-EnvironmentPath.ps1 function Remove-EnvironmentPath { [CmdletBinding()] param( [parameter(Mandatory=$true, Position=1, HelpMessage="The path to remove from the Path environmental variable.")] [string]$Path, [parameter(Mandatory=$false, Position=2, HelpMessage="The type of environment variable to target.")] [System.EnvironmentVariableTarget]$Target = [System.EnvironmentVariableTarget]::Process ) $oldPath = [System.Environment]::GetEnvironmentVariable('Path', $Target) if ($null -eq $oldPath) { return } $paths = [string[]]$oldPath.split(';') if ($paths -contains $Path) { $newPaths = $paths | Where-Object { $_ -ne $Path } $newPath = $newPaths -join ';' [System.Environment]::SetEnvironmentVariable('Path', $newPath, $Target) } } # END: source\os\Remove-EnvironmentPath.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 |