ACGCore.psm1
$script:__RNG = New-Object System.Random New-Alias -Name 'Save-Credential' -Value 'Save-PSCredential' New-Alias -Name 'Load-Credential' -Value 'Load-PSCredential' # CreateShortcut.ps1 function Create-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 { shoutOut "Invalid path: " Error -NoNewline shoutOut "$shortcutPath" Error 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 } # Generate new random SecureString to use as a password. function New-RandomString { [CmdletBinding()] param( [int]$Length=8, [string]$Characters="abcdefghijklmnopqrstuvwxyz0123456789-_", [switch]$AsSecureString ) $rng = $script:__RNG if ($AsSecureString) { $password = New-Object securestring for ($i = 0; $i -lt $Length; $i++) { $c = $Characters[$rng.Next($Characters.Length)] if ($rng.Next(10) -gt 4) { $c = "$c".ToUpper() } $password.AppendChar($c) } } else { $password = "" for ($i = 0; $i -lt $Length; $i++) { $c = $Characters[$rng.Next($Characters.Length)] if ($rng.Next(10) -gt 4) { $c = "$c".ToUpper() } $password += $c } } return $password } <# .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 Parse-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 } $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 } } <# .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 Render-Template, otherwise a new hashtable will be generated for each call to Render-Template. Recursive calls to Render-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" } Render-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 Render-Template{ [CmdletBinding()] param( [parameter( Mandatory=$true, HelpMessage="Path to the template file that should be rendered. Available when rendering." )] [String]$TemplatePath, [parameter( Mandatory=$true, HelpMessage="Hashtable with values used when interpolating expressions in the template. Available when rendering." )] [hashtable]$values, [Parameter( Mandatory=$false, 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 = '<<', [Parameter( Mandatory=$false, HelpMessage='Tag used to close interpolation sections. Regular expression.' )] [string]$EndTag = '>>' ) $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 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 } $templatePath = Resolve-Path $templatePath Write-Debug "Path resolved to '$templatePath'" $template = $null 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 = [System.IO.File]::ReadAllText($templatePath) $Cache[$templatePath] = @{ Value = $t; LoadTime = [datetime]::now } } } catch { <# Do nothing for now #> } $template = $Cache[$templatePath].Value } else { Write-Debug "Not in cache, loading..." $template = [System.IO.File]::ReadAllText($templatePath) $Cache[$templatePath] = @{ Value = $template; LoadTime = [datetime]::now } } # 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 = $templatePath | Split-Path -Parent if (!$__RenderCache[$templatePath].ContainsKey("Digest")) { $__buildDigest = { param($templateCache) 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) } } & $__buildDigest $__RenderCache[$templatePath] } # Expand values into user-space to make them more accessible during render. $values.GetEnumerator() | % { New-Variable $_.Name $_.Value } Write-Debug "Starting Render..." $__parts__ = $__RenderCache[$templatePath].Digest | % { $__part__ = $_ switch ($__part__.GetType()) { "hashtable" { if ($__part__.path) { Write-Debug "Including path..." $__c__ = Render-Template $__part__.path $Values if ($__part__.path -like "*.ps1") { $__s__ = [scriptblock]::create($__c__) try { $__s__.Invoke() } catch { $msg = "An unexpected exception occurred while Invoking '{0}' as part of '{1}'." -f $__part__.path, $templatePath $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 in '{0}': {1}" -f $templatePath, $__part__ $e = New-Object System.Exception $msg, $_.Exception throw $e } } default { $__part__ } } } $__parts__ -join "" } function Reset-Module { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string]$Name ) if ($module = Get-Module $Name) { Remove-Module $module -Force } Import-Module $Name -Global } <# .WISHLIST - Update so that that output is fed to shoutOut as it is generated rather than using the result output. The goal is to generate logging data continuously so that it's clear whether the script has hung or not. [Done] .SYNOPSIS Helper function to execute commands (strings or blocks) with error-handling/reporting. .DESCRIPTION Helper function to execute commands (strings or blocks) with error-handling/reporting. If a scriptblock is passed as the operation, the function will attempt make any variables referenced by the scriptblock available to the scriptblock when it is resolved (using variables available in the scope that called Run-Operation). The variables used in the command are identified using [scriptblock].Ast.FindAll method, and are imported from the parent scope using $PSCmdlet.SessionState.PSVariable.Get. The following variable-names are restricted and may cause errors if they are used in the operation: - $__thisOperation: The operation being run. - $__inputVariables: List of the variables being imported to run the operation. .NOTES - Transforms ScriptBlocks to Strings prior to execution because of a quirk in iex where it will not allow the evaluation of ScriptBlocks without an input (a 'param' statement in the block). iex is used because it yields output as each line is evaluated, rather than waiting for the entire $OPeration to complete as would be the case with <ScriptBlock>.Invoke(). #> function Run-Operation { param( [parameter(ValueFromPipeline=$true, position=1)] $Operation, [parameter()][Switch] $OutNull, [parameter()][Switch] $NotStrict, [parameter()][Switch] $LogErrorsOnly ) $color = "Result" if (!$NotStrict) { # Switch error action preference to catch any errors that might pop up. # Works so long as the internal operation doesn't also change the preference. $OldErrorActionPreference = $ErrorActionPreference $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop } if ($Operation -is [string]) { $OPeration = [scriptblock]::create($Operation) } if (!$LogErrorsOnly) { $msg = "Running '$Operation'..." $msg | shoutOut -MsgType Info -ContextLevel 1 } $r = try { # Step 1: Get any variables in the parent scope that are referenced by the operation. $localVarNames = Get-variable -Scope 0 | % Name if ($Operation -is [scriptblock]) { $variableNames = $Operation.Ast.FindAll( {param($o) $o -is [System.Management.Automation.Language.VariableExpressionAst]}, $true ) | % { $_.VariablePath.UserPath } | ? { $_ -notin $localVarNames } $variables = foreach ($vn in $variableNames) { $PSCmdlet.SessionState.PSVariable.Get($vn) } } # Step 2: Convert the scriptblock if necessary. if ($Operation -is [scriptblock]) { # Under certain circumstances the iex cmdlet will not allow # the evaluation of ScriptBlocks without an input. However it will evaluate strings # just fine so we perform the transformation before evaluation. $Operation = $Operation.ToString() } # Step 3: inject the operation and the variables into a new isolated scope and resolve # the operation there. & { param( $thisOperation, $inputVariables ) $__thisOperation = $thisOperation $__inputVariables = $inputVariables Remove-Variable "thisOperation" Remove-Variable "inputVariables" $__ = $null foreach ( $__ in $__inputVariables ) { if ($null -eq $__) { continue } Set-Variable $__.Name $__.Value } Remove-Variable "__" # Invoke-Expression allows us to receive # and handle output as it is generated, # rather than wait for the operation to finish # as opposed to <[scriptblock]>.invoke(). Invoke-Expression $__thisOperation | ForEach-Object { if (!$LogErrorsOnly) { shoutOut "`t| $_" "Result" -ContextLevel 2; } return $_ } } $Operation $variables } catch { $color = "Error" "An error occured while executing the operation:" | shoutOUt -MsgType Error -ContextLevel 1 $_.Exception, $_.CategoryInfo, $_.InvocationInfo, $_.ScriptStackTrace | Out-string | % { $_.Split("`n`r", [System.StringSplitOptions]::RemoveEmptyEntries).TrimEnd("`n`r") } | % { shoutOut "`t| $_" $color -ContextLevel 2 } $_ } if (!$NotStrict) { $ErrorActionPreference = $OldErrorActionPreference } if ($OutNull) { return } return $r } 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 } <# .SYNOPSIS Transforms a SecureString back into a plain string. Must the run by the same user, on the same computer where it was produced. #> function Unlock-SecureString { param( [Parameter(Mandatory=$true, ValueFromPipeline=$true)] [SecureString]$SecString ) $Marshal = [Runtime.InteropServices.Marshal] $bstr = $Marshal::SecureStringToBSTR($SecString) $r = $Marshal::ptrToStringAuto($bstr) $Marshal::ZeroFreeBSTR($bstr) return $r } 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 } 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 } } # ACGCore.credentials function Load-PSCredential { [CmdletBinding()] param( $Path, $Key ) $Path = Resolve-Path $path $credStr = [System.IO.File]::ReadAllText($path, [System.Text.Encoding]::UTF8) $u, $p = $credStr.split(":") $ConvertArgs = @{ String=$p } if ($key) { $keyBytes = [System.Convert]::FromBase64String($key) $ConvertArgs.Key = $keyBytes } New-Object PScredential $u, (ConvertTo-SecureString @ConvertArgs) } <# .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 } function Save-PSCredential( [PSCredential] $Credential, [string] $Path, [switch] $UseKey, [string] $Key ) { $convertArgs = @{ SecureString = $Credential.Password } if ($UseKey) { if ($Key) { $bytes = [System.Convert]::FromBase64String($Key) if ($bytes.count -ne 32) { throw "Invalid key provided for Save-Credential (expected a Base64 string convertable to a 32 byte array)." } } else { $r = [System.Random]::new() $bytes = [byte[]]( 0..31 | % { $r.next(0, 255) } ) } $convertArgs.Key = $bytes } $credStr = "{0}:{1}" -f $Credential.Username, (ConvertFrom-SecureString @convertArgs) $credStr | Out-File -FilePath $Path -Encoding utf8 if ($UseKey) { return [System.Convert]::ToBase64String($convertArgs.Key) } } # ACGCore.os 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 } } } # Utility to acquire registry values using reg.exe (uses Run-Operation) function Query-RegValue($key, $name){ $regValueQVregex = "\s+{0}\s+(?<type>REG_[A-Z]+)\s+(?<value>.*)" { reg query $key /v $name } | Run-Operation | ? { $_ -match ($regValueQVregex -f $name) } | % { $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 } } 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 } } } # 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) } 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" } | Run-Operation } default { { reg add $key /f /v $name /t $type /d $value } | Run-Operation } } } #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 = "$PSScripRoot\.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 } } $valeus.AutoLogonLimit = $AutoLogonLimit $tmpFile = [System.IO.Path]::GetTempFileName() Rendter-Template $templatePath $values > $tmpFile reg import $tmpFile Remove-Item $tmpFile } <# .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 Steal-RegKey { 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 ) } |Run-Operation 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) } | Run-Operation $me = [System.Security.Principal.NTAccount]$user $acl.SetOwner($me) { $key.SetAccessControl($acl) } | Run-Operation | Out-Null # After you have set owner you need to get the acl with the perms so you can modify it. $acl = { $key.GetAccessControl() } | Run-Operation $rule = New-Object System.Security.AccessControl.RegistryAccessRule ("BuiltIn\Administrators","FullControl","Allow") { $acl.SetAccessRule($rule) } | Run-Operation | Out-Null { $key.SetAccessControl($acl) } | Run-Operation | Out-Null $key.Close() shoutOut "Done!" Green } # ACGCore.polyfills # 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 } } } # ACGCore.text #ConvertFrom-UnicodeEscapedString.ps1 function ConvertFrom-UnicodeEscapedString { [CmdletBinding()] param( [Parameter(Mandatory=$true, ValueFromPipeline=$true)] [string]$InString ) return [System.Text.RegularExpressions.Regex]::Unescape($InString) } #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() } |