From 225b14f34b3f769ae5542dbd3ca6275b3ea242f3 Mon Sep 17 00:00:00 2001 From: Fabien Tschanz Date: Mon, 16 Feb 2026 14:25:59 +0100 Subject: [PATCH 1/2] Refactor core structure for improved maintainbility, add test cases --- ReverseDSC.Core.psm1 | 1174 ++++++++++++++++++------------- ReverseDSC.psd1 | 64 +- Tests/ReverseDSC.Core.Tests.ps1 | 789 +++++++++++++++++++++ 3 files changed, 1518 insertions(+), 509 deletions(-) create mode 100644 Tests/ReverseDSC.Core.Tests.ps1 diff --git a/ReverseDSC.Core.psm1 b/ReverseDSC.Core.psm1 index 38bb7e1..ecf7bb2 100644 --- a/ReverseDSC.Core.psm1 +++ b/ReverseDSC.Core.psm1 @@ -1,34 +1,81 @@ -$Global:CredsRepo = @() +$Script:CredsRepo = @() -function Get-DSCParamType +<# +.SYNOPSIS + Escapes a string for use in DSC configuration blocks. + +.DESCRIPTION + Applies standard escaping rules for backticks, European localized + quotation marks (U+201E, U+201C, U+201D), and double quotes. + Optionally preserves PowerShell variable expressions ($...). + +.PARAMETER InputString + The raw string value to escape. + +.PARAMETER AllowVariables + When specified, dollar signs ($) are not escaped, allowing + PowerShell variable expansion in the resulting string. +#> +function ConvertTo-EscapedDSCString { - <# + [CmdletBinding()] + [OutputType([System.String])] + param( + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [System.String] + $InputString, + + [Parameter()] + [switch] + $AllowVariables + ) + + if ([System.String]::IsNullOrEmpty($InputString)) + { + return [System.String]::Empty + } + + $result = $InputString.Replace('`', '``') + if (-not $AllowVariables) + { + $result = $result.Replace('$', '`$') + } + # Escape European localized quotation marks (U+201E „, U+201C “, U+201D ”) + $result = $result.Replace("$([char]0x201E)", "``$([char]0x201E)") + $result = $result.Replace("$([char]0x201C)", "``$([char]0x201C)") + $result = $result.Replace("$([char]0x201D)", "``$([char]0x201D)") + $result = $result.Replace("`"", "```"") + return $result +} + +<# .SYNOPSIS -Retrieves the data type of a specific parameter from the associated DSC -resource. + Retrieves the data type of a specific parameter from the associated DSC resource. .DESCRIPTION -This function scans the specified module (or in this case DSC resource), -checks for the specified parameter inside the .schema.mof file associated -with that module and properly assesses and returns the Data Type assigned -to the parameter. + This function scans the specified module (or in this case DSC resource), + checks for the specified parameter inside the .schema.mof file associated + with that module and properly assesses and returns the Data Type assigned + to the parameter. .PARAMETER ModulePath -Full file path to the .psm1 module we are looking for the property inside of. -In most cases this will be the full path to the .psm1 file of the DSC resource. + Full file path to the .psm1 module we are looking for the property inside of. + In most cases this will be the full path to the .psm1 file of the DSC resource. .PARAMETER ParamName -Name of the parameter in the module we want to determine the Data Type for. - + Name of the parameter in the module we want to determine the Data Type for. #> +function Get-DSCParamType +{ [CmdletBinding()] [OutputType([System.String])] param( - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [System.String] $ModulePath, - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [System.String] $ParamName ) @@ -38,45 +85,45 @@ Name of the parameter in the module we want to determine the Data Type for. $ast = [System.Management.Automation.Language.Parser]::ParseFile($ModulePath, [ref] $tokens, [ref] $errors) $functions = $ast.FindAll( { $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true) - ForEach ($function in $functions) + foreach ($function in $functions) { if ($function.Name -eq "Set-TargetResource") { $functionAst = [System.Management.Automation.Language.Parser]::ParseInput($function.Body, [ref] $tokens, [ref] $errors) $parameters = $functionAst.FindAll( { $args[0] -is [System.Management.Automation.Language.ParameterAst] }, $true) - ForEach ($parameter in $parameters) + foreach ($parameter in $parameters) { if ($parameter.Name.Extent.Text -eq $ParamName) { $attributes = $parameter.Attributes - ForEach ($attribute in $attributes) + foreach ($attribute in $attributes) { if ($attribute.TypeName.FullName -like "System.*") { return $attribute.TypeName.FullName } - elseif ($attribute.TypeName.FullName.ToLower() -eq "microsoft.management.infrastructure.ciminstance") + elseif ($attribute.TypeName.FullName -eq "Microsoft.Management.Infrastructure.CimInstance") { return "System.Collections.Hashtable" } - elseif ($attribute.TypeName.FullName.ToLower() -eq "string") + elseif ($attribute.TypeName.FullName -eq "string") { return "System.String" } - elseif ($attribute.TypeName.FullName.ToLower() -eq "boolean") + elseif ($attribute.TypeName.FullName -eq "boolean") { return "System.Boolean" } - elseif ($attribute.TypeName.FullName.ToLower() -eq "bool") + elseif ($attribute.TypeName.FullName -eq "bool") { return "System.Boolean" } - elseif ($attribute.TypeName.FullName.ToLower() -eq "string[]") + elseif ($attribute.TypeName.FullName -eq "string[]") { return "System.String[]" } - elseif ($attribute.TypeName.FullName.ToLower() -eq "microsoft.management.infrastructure.ciminstance[]") + elseif ($attribute.TypeName.FullName -eq "Microsoft.Management.Infrastructure.CimInstance[]") { return "Microsoft.Management.Infrastructure.CimInstance[]" } @@ -87,6 +134,354 @@ Name of the parameter in the module we want to determine the Data Type for. } } +<# +.SYNOPSIS + Converts a string parameter value to its DSC representation. + +.PARAMETER Value + The string value to convert. + +.PARAMETER NoEscape + If true, the string will not be escaped. + +.PARAMETER AllowVariables + If true, PowerShell variables ($...) are preserved in the string. +#> +function ConvertTo-DSCStringValue +{ + [CmdletBinding()] + [OutputType([System.String])] + param( + [Parameter()] + [System.String] + $Value, + + [Parameter()] + [System.Boolean] + $NoEscape = $false, + + [Parameter()] + [System.Boolean] + $AllowVariables = $false + ) + + if ($null -eq $Value) + { + return '""' + } + + if ($NoEscape) + { + return $Value + } + + $escapedString = ConvertTo-EscapedDSCString -InputString $Value -AllowVariables:$AllowVariables + return "`"$escapedString`"" +} + +<# +.SYNOPSIS + Converts a boolean parameter value to its DSC representation. + +.PARAMETER Value + The boolean value to convert. +#> +function ConvertTo-DSCBooleanValue +{ + [CmdletBinding()] + [OutputType([System.String])] + param( + [Parameter(Mandatory = $true)] + [System.Boolean] + $Value + ) + + return "`$$Value" +} + +<# +.SYNOPSIS + Converts a PSCredential parameter value to its DSC representation. + +.PARAMETER Value + The PSCredential value to convert. + +.PARAMETER ParameterName + The name of the parameter (used as fallback). +#> +function ConvertTo-DSCCredentialValue +{ + [CmdletBinding()] + [OutputType([System.String])] + param( + [Parameter()] + [System.Management.Automation.PSCredential] + $Value, + + [Parameter(Mandatory = $true)] + [System.String] + $ParameterName + ) + + if ($null -eq $Value) + { + return "Get-Credential -Message $ParameterName" + } + + $credString = $Value.ToString() + if ($credString -like "`$Creds*") + { + return $credString.Replace("-", "_").Replace(".", "_") + } + + $userName = $Value.UserName + if ($null -eq $userName) + { + $userName = ($credString.Split('\'))[1] + } + + if ($userName.Contains("@") -and -not $userName.Contains("\")) + { + $cleanName = ($userName.Split('@'))[0] + } + else + { + $cleanName = ($userName.Split('\'))[-1] + } + + $cleanName = $cleanName.Replace("-", "_").Replace(".", "_").Replace(" ", "").Replace("@", "") + return "`$Creds$cleanName" +} + +<# +.SYNOPSIS + Converts a hashtable parameter value to its DSC representation. + +.PARAMETER Value + The hashtable to convert. +#> +function ConvertTo-DSCHashtableValue +{ + [CmdletBinding()] + [OutputType([System.String])] + param( + [Parameter(Mandatory = $true)] + [System.Collections.IDictionary] + $Value + ) + + $result = "@{" + foreach ($key in $Value.Keys) + { + try + { + $result += "$key = `"$($Value[$key])`"; " + } + catch + { + return $Value + } + } + $result += "}" + return $result +} + +<# +.SYNOPSIS + Converts a string array parameter value to its DSC representation. + +.PARAMETER Value + The array to convert. + +.PARAMETER NoEscape + If true, array elements will not be escaped. + +.PARAMETER AllowVariables + If true, PowerShell variables in strings are preserved. +#> +function ConvertTo-DSCStringArrayValue +{ + [CmdletBinding()] + [OutputType([System.String])] + param( + [Parameter()] + [System.Object[]] + $Value, + + [Parameter()] + [System.Boolean] + $NoEscape = $false, + + [Parameter()] + [System.Boolean] + $AllowVariables = $false + ) + + if ($null -eq $Value -or $Value.Count -eq 0) + { + return "@()" + } + + $result = "@(" + foreach ($item in $Value) + { + if ($null -ne $item) + { + if ($NoEscape) + { + $innerValue = $item + } + else + { + $innerValue = ConvertTo-EscapedDSCString -InputString $item -AllowVariables:$AllowVariables + } + $result += "`"$innerValue`"," + } + } + + if ($result.Length -gt 2 -and $result.EndsWith(",")) + { + $result = $result.Substring(0, $result.Length - 1) + } + $result += ")" + return $result +} + +<# +.SYNOPSIS + Converts an integer array parameter value to its DSC representation. + +.PARAMETER Value + The integer array to convert. +#> +function ConvertTo-DSCIntegerArrayValue +{ + [CmdletBinding()] + [OutputType([System.String])] + param( + [Parameter()] + [System.Object[]] + $Value + ) + + if ($null -eq $Value -or $Value.Count -eq 0) + { + return "@()" + } + + return "@($($Value -join ','))" +} + +<# +.SYNOPSIS + Converts an object array parameter value to its DSC representation. + +.PARAMETER Value + The array to convert. + +.PARAMETER NoEscape + If true, string elements will not be escaped. + +.PARAMETER AllowVariables + If true, PowerShell variables in strings are preserved. +#> +function ConvertTo-DSCObjectArrayValue +{ + [CmdletBinding()] + [OutputType([System.String])] + param( + [Parameter()] + [System.Object[]] + $Value, + + [Parameter()] + [System.Boolean] + $NoEscape = $false, + + [Parameter()] + [System.Boolean] + $AllowVariables = $false + ) + + if ($null -eq $Value -or $Value.Count -eq 0) + { + return "@()" + } + + # Handle string arrays + if ($Value[0].GetType().Name -eq "String") + { + $result = "@(" + foreach ($item in $Value) + { + if ($NoEscape) + { + $result += $item + } + else + { + $escapedString = ConvertTo-EscapedDSCString -InputString $item -AllowVariables:$AllowVariables + $result += "`"$escapedString`"," + } + } + + # Remove the trailing comma if it exists + if ($result.Length -gt 2 -and $result.EndsWith(",")) + { + $result = $result.Substring(0, $result.Length - 1) + } + $result += ")" + return $result + } + + # Handle hashtable arrays + if ($Value[0].GetType().Name -eq "Hashtable") + { + $result = "@(" + foreach ($hashtable in $Value) + { + $result += "@{" + foreach ($pair in $hashtable.GetEnumerator()) + { + if ($pair.Value -is [System.Array]) + { + $str = "$($pair.Key)=@('$($pair.Value -join "', '")')" + } + else + { + if ($null -eq $pair.Value) + { + $str = "$($pair.Key)=`$null" + } + else + { + $str = "$($pair.Key)='$($pair.Value)'" + } + } + $result += "$str; " + } + + # Remove the trailing semicolon and space if they exist + if ($result.Length -gt 2 -and $result.EndsWith("; ")) + { + $result = $result.Substring(0, $result.Length - 2) + } + $result += "}" + } + $result += ")" + return $result + } + + # Default handling for other object arrays + $result = "@(" + foreach ($item in $Value) + { + $result += $item + } + $result += ")" + return $result +} + <# .SYNOPSIS Generate the DSC string representing the resource's instance. @@ -126,8 +521,8 @@ function Get-DSCBlock ) # Sort the params by name(key), exclude _metadata_* properties (coming from DSCParser) - $Sorted = $Params.GetEnumerator() | Sort-Object -Property Name | Where-Object {$_.Name -notlike '_metadata_*'} - $NewParams = [Ordered]@{} + $Sorted = $Params.GetEnumerator() | Where-Object Name -NotLike '_metadata_*' | Sort-Object -Property Name + $NewParams = [ordered]@{} foreach ($entry in $Sorted) { @@ -137,7 +532,7 @@ function Get-DSCBlock } } - # Figure out what parameter has the longuest name, and get its Length; + # Figure out what parameter has the longest name, and get its Length; $maxParamNameLength = 0 foreach ($param in $NewParams.Keys) { @@ -147,348 +542,133 @@ function Get-DSCBlock } } - # PSDscRunAsCredential is 20 characters and in most case the longuest. + # PSDscRunAsCredential is 20 characters and in most case the longest. if ($maxParamNameLength -lt 20) { $maxParamNameLength = 20 } - $dscBlock = [System.Text.StringBuilder]::New() + $dscBlock = [System.Text.StringBuilder]::new() $NewParams.Keys | ForEach-Object { - if ($null -ne $NewParams[$_]) + $paramName = $_ + $paramValue = $NewParams[$paramName] + + if ($null -ne $paramValue) { - $paramType = $NewParams[$_].GetType().Name + $paramType = $paramValue.GetType().Name } else { - $paramType = Get-DSCParamType -ModulePath $ModulePath -ParamName "`$$_" + $paramType = Get-DSCParamType -ModulePath $ModulePath -ParamName "`$$paramName" } + $isNoEscape = $NoEscape -contains $paramName $value = $null - if ($paramType -eq "System.String" -or $paramType -eq "String" -or $paramType -eq "Guid" -or $paramType -eq 'TimeSpan' -or $paramType -eq 'DateTime') + + # Dispatch to type-specific converter + switch -Regex ($paramType) { - if ($null -ne $NewParams.Item($_)) - { - if ($NoEscape -contains $_) - { - $value = $NewParams.Item($_).ToString() - } - else - { - #0x201E = „ - #0x201C = “ - #0x201D = ” - if ($AllowVariablesInStrings) - { - $newString = $NewParams.Item($_).ToString().Replace('`', '``') - $newString = $newString.Replace("$([char]0x201E)", "``$([char]0x201E)") - $newString = $newString.Replace("$([char]0x201C)", "``$([char]0x201C)") - $newString = $newString.Replace("$([char]0x201D)", "``$([char]0x201D)") - $newString = $newString.Replace("`"", "```"") - $value = "`"" + $newString + "`"" - } - else - { - $newString = $NewParams.Item($_).ToString().Replace('`', '``').Replace('$', '`$') - $newString = $newString.Replace("$([char]0x201E)", "``$([char]0x201E)") - $newString = $newString.Replace("$([char]0x201C)", "``$([char]0x201C)") - $newString = $newString.Replace("$([char]0x201D)", "``$([char]0x201D)") - $newString = $newString.Replace("`"", "```"") - $value = "`"" + $newString + "`"" - } - } - } - else + '^(System\.String|String|Guid|TimeSpan|DateTime)$' { - $value = '""' + $value = ConvertTo-DSCStringValue -Value $paramValue -NoEscape $isNoEscape -AllowVariables $AllowVariablesInStrings } - } - elseif ($paramType -eq "System.Boolean" -or $paramType -eq "Boolean") - { - $value = "`$" + $NewParams.Item($_) - } - elseif ($paramType -eq "System.Management.Automation.PSCredential") - { - if ($null -ne $NewParams.Item($_)) + '^(System\.Boolean|Boolean)$' { - if ($NewParams.Item($_).ToString() -like "`$Creds*") - { - $value = $NewParams.Item($_).Replace("-", "_").Replace(".", "_") - } - else - { - if ($null -eq $NewParams.Item($_).UserName) - { - $value = "`$Creds" + ($NewParams.Item($_).Split('\'))[1].Replace("-", "_").Replace(".", "_") - } - else - { - if ($NewParams.Item($_).UserName.Contains("@") -and -not $NewParams.Item($_).UserName.Contains("\")) - { - $value = "`$Creds" + ($NewParams.Item($_).UserName.Split('@'))[0] - } - else - { - $value = "`$Creds" + ($NewParams.Item($_).UserName.Split('\'))[1].Replace("-", "_").Replace(".", "_") - } - } - } + $value = ConvertTo-DSCBooleanValue -Value $paramValue } - else + '^System\.Management\.Automation\.PSCredential$' { - $value = "Get-Credential -Message " + $_ - } - } - elseif ($paramType -eq "System.Collections.Hashtable" -or $paramType -eq "Hashtable") - { - $value = "@{" - $hash = $NewParams.Item($_) - $hash.Keys | ForEach-Object { - try - { - $value += $_.ToString() + " = `"" + $hash.Item($_).ToString() + "`"; " - } - catch - { - $value = $hash - } + $value = ConvertTo-DSCCredentialValue -Value $paramValue -ParameterName $paramName } - $value += "}" - } - elseif ($paramType -eq "System.String[]" -or $paramType -eq "String[]" -or $paramType -eq "ArrayList" -or $paramType -eq "List``1") - { - $hash = $NewParams.Item($_) - if ($hash -and -not $hash.ToString().StartsWith("`$ConfigurationData.")) + '^(System\.Collections\.Hashtable|Hashtable)$' { - $value = "@(" - $hash | ForEach-Object { - if ($null -ne $_) - { - if ($NoEscape -contains $hash) - { - $innerValue = $_ - } - else - { - #0x201E = „ - #0x201C = “ - #0x201D = ” - if ($AllowVariablesInStrings) - { - $newString = $_.Replace('`', '``') - $newString = $newString.Replace("$([char]0x201E)", "``$([char]0x201E)") - $newString = $newString.Replace("$([char]0x201C)", "``$([char]0x201C)") - $newString = $newString.Replace("$([char]0x201D)", "``$([char]0x201D)") - $newString = $newString.Replace("`"", "```"") - $innerValue = $newString - } - else - { - $newString = $_.Replace('`', '``').Replace('$', '`$') - $newString = $newString.Replace("$([char]0x201E)", "``$([char]0x201E)") - $newString = $newString.Replace("$([char]0x201C)", "``$([char]0x201C)") - $newString = $newString.Replace("$([char]0x201D)", "``$([char]0x201D)") - $newString = $newString.Replace("`"", "```"") - $innerValue = $newString - } - } - } - $value += "`"" + $innerValue + "`"," - } - if ($value.Length -gt 2) - { - $value = $value.Substring(0, $value.Length - 1) - } - $value += ")" + $value = ConvertTo-DSCHashtableValue -Value $paramValue } - else + '^(System\.String\[\]|String\[\]|ArrayList|List``1)$' { - if ($hash) + if ($paramValue.ToString().StartsWith("`$ConfigurationData.")) { - $value = $hash + $value = $paramValue } else { - $value = "@()" + $value = ConvertTo-DSCStringArrayValue -Value $paramValue -NoEscape $isNoEscape -AllowVariables $AllowVariablesInStrings } } - } - elseif ($paramType -match "Int.*\[\]") - { - $hash = $NewParams.Item($_) - if ($hash) + 'Int.*\[\]' { - $value = "@(" - $hash | ForEach-Object { - $value += $_.ToString() + "," - } - if ($value.Length -gt 2) - { - $value = $value.Substring(0, $value.Length - 1) - } - $value += ")" + $value = ConvertTo-DSCIntegerArrayValue -Value $paramValue } - else + '^(Object\[\]|Microsoft\.Management\.Infrastructure\.CimInstance\[\])$' { - if ($hash) + if ($paramType -ne "Microsoft.Management.Infrastructure.CimInstance[]" -and + $paramValue.Length -gt 0 -and $paramValue[0].GetType().Name -eq "String") { - $value = $hash + $value = ConvertTo-DSCObjectArrayValue -Value $paramValue -NoEscape $isNoEscape -AllowVariables $AllowVariablesInStrings } else { - $value = "@()" + $value = ConvertTo-DSCObjectArrayValue -Value $paramValue -NoEscape $isNoEscape -AllowVariables $AllowVariablesInStrings } } - } - elseif ($paramType -eq "Object[]" -or $paramType -eq "Microsoft.Management.Infrastructure.CimInstance[]") - { - $array = $hash = $NewParams.Item($_) - - if ($array.Length -gt 0 -and ($null -ne $array[0] -and $array[0].GetType().Name -eq "String" -and $paramType -ne "Microsoft.Management.Infrastructure.CimInstance[]")) + '^CimInstance$' { - $value = "@(" - $paramName = $_ - $hash | ForEach-Object { - if ($NoEscape -contains $paramName) - { - $value += $_.ToString() - } - else - { - #0x201E = „ - #0x201C = “ - #0x201D = ” - if ($AllowVariablesInStrings) - { - $newString = $_.ToString().Replace('`', '``') - $newString = $newString.Replace("$([char]0x201E)", "``$([char]0x201E)") - $newString = $newString.Replace("$([char]0x201C)", "``$([char]0x201C)") - $newString = $newString.Replace("$([char]0x201D)", "``$([char]0x201D)") - $newString = $newString.Replace("`"", "```"") - $value += "`"" + $newString + "`"," - } - else - { - $newString = $_.ToString().Replace('`', '``').Replace('$', '`$') - $newString = $newString.Replace("$([char]0x201E)", "``$([char]0x201E)") - $newString = $newString.Replace("$([char]0x201C)", "``$([char]0x201C)") - $newString = $newString.Replace("$([char]0x201D)", "``$([char]0x201D)") - $newString = $newString.Replace("`"", "```"") - $value += "`"" + $newString + "`"," - } - } - } - # Remove trailing comma if it exists - if ($value.Length -gt 2 -and $value.EndsWith(",")) - { - $value = $value.Substring(0, $value.Length - 1) - } - $value += ")" + $value = $paramValue } - elseif ($array.Length -gt 0 -and ($null -ne $array[0] -and $array[0].GetType().Name -eq "Hashtable")) + default { - $value = "@(" - foreach ($hashtable in $array) + if ($null -eq $paramValue) { - $value += "@{" - foreach ($pair in $Hashtable.GetEnumerator()) - { - if ($pair.Value -is [System.Array]) - { - $str = "$($pair.Key)=@('$($pair.Value-join "', '")')" - } - else - { - if ($null -eq $pair.Value) - { - $str = "$($pair.Key)=`$null" - } - else - { - $str = "$($pair.Key)='$($pair.Value)'" - } - } - $value += "$str; " - } - # Remove trailing semicolon if it exists - if ($value.Length -gt 2 -and $value.EndsWith("; ")) - { - $value = $value.Substring(0, $value.Length - 2) - } - $value += "}" + $value = "`$null" } - $value += ")" - } - else - { - $value = "@(" - $array | ForEach-Object { - $value += $_ - } - $value += ")" - } - } - elseif ($paramType -eq "CimInstance") - { - $value = $NewParams[$_] - } - else - { - if ($null -eq $NewParams[$_]) - { - $value = "`$null" - } - else - { - if ($NewParams[$_].GetType().BaseType.Name -eq "Enum") + elseif ($paramValue.GetType().BaseType.Name -eq "Enum") { - $value = "`"" + $NewParams.Item($_) + "`"" + $value = "`"$paramValue`"" } else { - $value = "$($NewParams.Item($_))" + $value = "$paramValue" } } } # Determine the number of additional spaces we need to add before the '=' to make sure the values are all aligned. This number - # is obtained by substracting the length of the current parameter's name to the maximum length found. - $numberOfAdditionalSpaces = $maxParamNameLength - $_.Length - $additionalSpaces = "" - for ($i = 0; $i -lt $numberOfAdditionalSpaces; $i++) + # is obtained by subtracting the length of the current parameter's name from the maximum length found. + $numberOfAdditionalSpaces = $maxParamNameLength - $paramName.Length + $additionalSpaces = " " * $numberOfAdditionalSpaces + + # Check for comment/metadata and insert it back here + $PropertyMetadataKeyName = "_metadata_$paramName" + if ($Params.ContainsKey($PropertyMetadataKeyName)) { - $additionalSpaces += " " + $CommentValue = ' ' + $Params[$PropertyMetadataKeyName] } - # Check for comment/metadata and insert it back here - $PropertyMetadataKeyName="_metadata_$($_)" - if ($Params.ContainsKey($PropertyMetadataKeyName)) { - $CommentValue=' '+$Params[$PropertyMetadataKeyName] - } Else { - $CommentValue='' + else + { + $CommentValue = '' } - [void]$dscBlock.Append(" " + $_ + $additionalSpaces + " = " + $value + ";" + $CommentValue + "`r`n") + [void]$dscBlock.Append(" $paramName$additionalSpaces = $value;$CommentValue`r`n") } return $dscBlock.ToString() } -function Get-DSCFakeParameters -{ - <# +<# .SYNOPSIS -Generates a hashtable containing all the properties exposed by the specified -DSC resource but with fake values. + Generates a hashtable containing all the properties exposed by the + specified DSC resource but with fake values. .DESCRIPTION -This function scans the specified resources, create a hashtable with all the -properties it exposes and generates fake values for each property based on -the Data Type assigned to it. + This function scans the specified resource, creates a hashtable with all + the properties it exposes and generates fake values for each property + based on the Data Type assigned to it. .PARAMETER ModulePath -Full file path to the .psm1 module we are looking to get an instance of. -In most cases this will be the full path to the .psm1 file of the DSC resource. - + Full file path to the .psm1 module we are looking to get an instance of. + In most cases this will be the full path to the .psm1 file of the DSC resource. #> +function Get-DSCFakeParameters +{ [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param( @@ -565,24 +745,23 @@ In most cases this will be the full path to the .psm1 file of the DSC resource. return $params } -function Get-DSCDependsOnBlock -{ - <# +<# .SYNOPSIS -Generates a string that represents the DependsOn clause based on the received -list of dependencies. + Generates a string that represents the DependsOn clause based on the + received list of dependencies. .DESCRIPTION -This function receives an array of string that represents the list of DSC -resource dependencies for the current DSC block and generates a string -that represents the associated DependsOn DSC string. + This function receives an array of strings that represents the list of DSC + resource dependencies for the current DSC block and generates a string + that represents the associated DependsOn DSC string. .PARAMETER DependsOnItems -Array of string values that represent the list of depdencies for the -current DSC block. Object in the array are expected to be in the form of: -[]. - + Array of string values that represent the list of dependencies for the + current DSC block. Objects in the array are expected to be in the form of: + []. #> +function Get-DSCDependsOnBlock +{ [CmdletBinding()] [OutputType([System.String])] param( @@ -600,24 +779,22 @@ current DSC block. Object in the array are expected to be in the form of: return $dependsOnClause } -<# Region Helper Methods #> -function Get-Credentials -{ - <# +<# .SYNOPSIS -Returns the full username of (\) of the specified user -if it is already stroed in our credentials hashtable. + Returns the full username (\) of the specified user + if it is already stored in the credentials hashtable. .DESCRIPTION -This function checks in the hashtable that stores all the required -credentials (service account, etc.) for our configuration and -returns the fully formatted username. + This function checks in the hashtable that stores all the required + credentials (service account, etc.) for the configuration and + returns the fully formatted username. .PARAMETER UserName -Name of the user we wish to check to see if it is already stored in our -credentials hashtable. - + Name of the user we wish to check to see if it is already stored in our + credentials hashtable. #> +function Get-Credentials +{ [CmdletBinding()] [OutputType([System.String])] param( @@ -626,31 +803,31 @@ credentials hashtable. $UserName ) - if ($Global:CredsRepo.Contains($UserName.ToLower())) + if ($Script:CredsRepo.Contains($UserName.ToLower())) { return $UserName.ToLower() } return $null } -function Resolve-Credentials -{ - <# +<# .SYNOPSIS -Returns a string representing the name of the PSCredential variable -associated with the specific username. + Returns a string representing the name of the PSCredential variable + associated with the specified username. .DESCRIPTION -This function takes in a specified user name and returns what the standardized -variable name for that user should be inside of our extracted DSC configuration. -Credentials variables will always be named $Creds as a standard for -ReverseDSC. This function makes sure that the variable name doesn't contain -character that are invalid in variable names bu might be valid in Usernames. + This function takes in a specified user name and returns what the + standardized variable name for that user should be inside of our + extracted DSC configuration. Credential variables will always be named + $Creds as a standard for ReverseDSC. This function makes sure + that the variable name does not contain characters that are invalid in + variable names but might be valid in usernames. .PARAMETER UserName -Name of the user we wish to get the associated variable name from. - + Name of the user we wish to get the associated variable name from. #> +function Resolve-Credentials +{ [CmdletBinding()] [OutputType([System.String])] param( @@ -666,48 +843,46 @@ Name of the user we wish to get the associated variable name from. return "`$Creds" + $UserName.Replace("-", "_").Replace(".", "_").Replace(" ", "").Replace("@", "") } -function Save-Credentials -{ - <# +<# .SYNOPSIS -Adds the specified username to our central list of required credentials. + Adds the specified username to our central list of required credentials. .DESCRIPTION -This function checks to see if the specified user is already stored in our -central required credentials list, and if not simply adds it to it. + This function checks to see if the specified user is already stored in our + central required credentials list, and if not simply adds it to it. .PARAMETER UserName -Username to add to the central list of required credentials. - + Username to add to the central list of required credentials. #> +function Save-Credentials +{ [CmdletBinding()] param( [Parameter(Mandatory = $true)] [System.String] $UserName ) - if (-not $Global:CredsRepo.Contains($UserName.ToLower())) + if (-not $Script:CredsRepo.Contains($UserName.ToLower())) { - $Global:CredsRepo += $UserName.ToLower() + $Script:CredsRepo += $UserName.ToLower() } } -function Test-Credentials -{ - <# +<# .SYNOPSIS -Checks to see if the specified username if already in our central list of -required credentials. + Checks to see if the specified username is already in our central list of + required credentials. .DESCRIPTION -This function checks the central list of required credentials to see if the -specified user is already part of it. If it finds it, it returns $true, -otherwise it returns false. + This function checks the central list of required credentials to see if + the specified user is already part of it. If it finds it, it returns + $true, otherwise it returns $false. .PARAMETER UserName -Username to check for existence in the central list of required users. - + Username to check for existence in the central list of required users. #> +function Test-Credentials +{ [CmdletBinding()] [OutputType([System.Boolean])] param( @@ -715,48 +890,47 @@ Username to check for existence in the central list of required users. [System.String] $UserName ) - if ($Global:CredsRepo.Contains($UserName.ToLower())) + if ($Script:CredsRepo.Contains($UserName.ToLower())) { return $true } return $false } -function Convert-DSCStringParamToVariable -{ - <# +<# .SYNOPSIS -Removes quotes around a parameter in the resulting DSC config, -effectively converting it to a variable instead of a string value. + Removes quotes around a parameter in the resulting DSC config, + effectively converting it to a variable instead of a string value. .DESCRIPTION -This function will scan the content of the current DSC block for the -resource, find the specified parameter and remove quotes around its -value so that it becomes a variable instead of a string value. + This function will scan the content of the current DSC block for the + resource, find the specified parameter and remove quotes around its + value so that it becomes a variable instead of a string value. .PARAMETER DSCBlock -The string representation of the current DSC resource instance we -are extracting along with all of its parameters and values. + The string representation of the current DSC resource instance we + are extracting along with all of its parameters and values. .PARAMETER ParameterName -The name of the parameter we wish to convert the value as a variable -instead of a string value for. + The name of the parameter we wish to convert the value as a variable + instead of a string value for. .PARAMETER IsCIMArray -Represents whether or not the parameter to convert to a variable is an -array of CIM instances or not. We need to differentiate by explicitely -passing in this parameter because to the function a CIMArray is nothing -but a System.Object[] and will threat it as it. CIMArray differ in that -we should not have commas in between items it contains. + Represents whether or not the parameter to convert to a variable is an + array of CIM instances or not. We need to differentiate by explicitly + passing in this parameter because to the function a CIMArray is nothing + but a System.Object[] and will treat it as such. CIMArrays differ in + that we should not have commas in between items they contain. .PARAMETER IsCIMObject -Represents whether or not the parameter to convert to a variable is a -CIM instance or not. We need to differentiate by explicitely passing -in this parameter because to the function a CIMArray is nothing -but a String object and will threat it as it. However it has escaped -double quotes, which need to be handled properly. - + Represents whether or not the parameter to convert to a variable is a + CIM instance or not. We need to differentiate by explicitly passing + in this parameter because to the function a CIMObject is nothing + but a String object and will treat it as such. However it has escaped + double quotes, which need to be handled properly. #> +function Convert-DSCStringParamToVariable +{ [CmdletBinding()] [OutputType([System.String])] param( @@ -914,41 +1088,38 @@ double quotes, which need to be handled properly. return $DSCBlock } -<# Region ConfigurationData Methods #> -$ConfigurationDataContent = @{} +$Script:ConfigurationDataContent = @{} -function Add-ConfigurationDataEntry -{ - <# +<# .SYNOPSIS -Adds a property to the resulting ConfigurationData file from the -extract. + Adds a property to the resulting ConfigurationData file from the extract. .DESCRIPTION -This function helps build the hashtable that will eventually result -in the ConfigurationData .psd1 file generated by the extraction of -the configuration. It allows you to speficy what section to add it -to inside the hashtable, and allows you to speficy a description for -each one. These description will eventually become comments that -will appear on top of the property in the ConfigurationData file. + This function helps build the hashtable that will eventually result + in the ConfigurationData .psd1 file generated by the extraction of + the configuration. It allows you to specify what section to add it + to inside the hashtable, and allows you to specify a description for + each one. These descriptions will eventually become comments that + will appear on top of the property in the ConfigurationData file. .PARAMETER Node -Specifies the node entry under which we want to add this parameter -under. You can also specify NonNodeData names to have the property -added under custom non-node specific section. + Specifies the node entry under which we want to add this parameter. + You can also specify NonNodeData names to have the property added + under custom non-node specific sections. .PARAMETER Key -The name of the parameter to add. + The name of the parameter to add. .PARAMETER Value -The value of the parameter to add. + The value of the parameter to add. .PARAMETER Description -Description of the parameter to add. This will ultimately appear in -the generated ConfigurationData .psd1 file as a comment appearing on -top of the parameter. - + Description of the parameter to add. This will ultimately appear in + the generated ConfigurationData .psd1 file as a comment appearing on + top of the parameter. #> +function Add-ConfigurationDataEntry +{ [CmdletBinding()] param( [Parameter(Mandatory = $true)] @@ -968,37 +1139,36 @@ top of the parameter. $Description ) - if ($null -eq $ConfigurationDataContent[$Node]) + if ($null -eq $Script:ConfigurationDataContent[$Node]) { - $ConfigurationDataContent.Add($Node, @{}) - $ConfigurationDataContent[$Node].Add("Entries", [ordered]@{}) + $Script:ConfigurationDataContent.Add($Node, @{}) + $Script:ConfigurationDataContent[$Node].Add("Entries", [ordered]@{}) } - $ConfigurationDataContent[$Node].Entries[$Key] = @{ Value = $Value; Description = $Description } + $Script:ConfigurationDataContent[$Node].Entries[$Key] = @{ Value = $Value; Description = $Description } } -function Get-ConfigurationDataEntry -{ - <# +<# .SYNOPSIS -Retrieves the value of a given property in the specified node/section -from the hashtable that is being dynamically built. + Retrieves the value of a given property in the specified node/section + from the hashtable that is being dynamically built. .DESCRIPTION -This function will return the value of the specified parameter from the -hash table being dynamically built and which will ultimately become the -content of the ConfigurationData .psd1 file being generated. + This function will return the value of the specified parameter from the + hashtable being dynamically built and which will ultimately become the + content of the ConfigurationData .psd1 file being generated. .PARAMETER Node -The name of the node or section in the Hashtable we want to look for -the key in. + The name of the node or section in the hashtable we want to look for + the key in. .PARAMETER Key -The name of the parameter to retrieve the value from. - + The name of the parameter to retrieve the value from. #> +function Get-ConfigurationDataEntry +{ [CmdletBinding()] - [OutputType([System.String])] + [OutputType([System.Collections.Hashtable])] param( [Parameter(Mandatory = $true)] [System.String] @@ -1011,49 +1181,64 @@ The name of the parameter to retrieve the value from. <# If node is null, then search in all nodes and return first result found. #> if ($null -eq $Node) { - foreach ($Node in $ConfigurationDataContent.Keys) + foreach ($Node in $Script:ConfigurationDataContent.Keys) { - if ($ConfigurationDataContent[$Node].Entries.ContainsKey($Key)) + if ($Script:ConfigurationDataContent[$Node].Entries.Contains($Key)) { - return $ConfigurationDataContent[$Node].Entries[$Key] + return $Script:ConfigurationDataContent[$Node].Entries[$Key] } } } else { - if ($ConfigurationDataContent[$Node].Entries.ContainsKey($Key)) + if ($Script:ConfigurationDataContent.ContainsKey($Node) -and $Script:ConfigurationDataContent[$Node].Entries.Contains($Key)) { - return $ConfigurationDataContent[$Node].Entries[$Key] + return $Script:ConfigurationDataContent[$Node].Entries[$Key] } } } -function Get-ConfigurationDataContent -{ - <# +<# .SYNOPSIS -Retrieves the entire content of the ConfigurationData file being -dynamically generated. + Clears the content of the hashtable that is being dynamically built for the ConfigurationData .psd1 file. .DESCRIPTION -This function will return the content of the dynamically built -hashtable for the ConfigurationData content as a formatted string. + This function will clear the content of the hashtable that is being built + for the ConfigurationData .psd1 file, effectively resetting it to an empty + state. +#> +function Clear-ConfigurationDataContent +{ + [CmdletBinding()] + param() + $Script:ConfigurationDataContent = @{} +} +<# +.SYNOPSIS + Retrieves the entire content of the ConfigurationData file being + dynamically generated. + +.DESCRIPTION + This function will return the content of the dynamically built + hashtable for the ConfigurationData content as a formatted string. #> +function Get-ConfigurationDataContent +{ [CmdletBinding()] [OutputType([System.String])] param() $psd1Content = "@{`r`n" $psd1Content += " AllNodes = @(`r`n" - foreach ($node in $ConfigurationDataContent.Keys.Where{ $_.ToLower() -ne "nonnodedata" }) + foreach ($node in $Script:ConfigurationDataContent.Keys.Where{ $_ -ne "NonNodeData" }) { $psd1Content += " @{`r`n" $psd1Content += " NodeName = `"" + $node + "`"`r`n" $psd1Content += " PSDscAllowPlainTextPassword = `$true;`r`n" $psd1Content += " PSDscAllowDomainUser = `$true;`r`n" $psd1Content += " #region Parameters`r`n" - $keyValuePair = $ConfigurationDataContent[$node].Entries + $keyValuePair = $Script:ConfigurationDataContent[$node].Entries foreach ($key in $keyValuePair.Keys | Sort-Object) { if ($null -ne $keyValuePair[$key].Description) @@ -1084,10 +1269,10 @@ hashtable for the ConfigurationData content as a formatted string. $psd1Content += " )`r`n" $psd1Content += " NonNodeData = @(`r`n" - foreach ($node in $ConfigurationDataContent.Keys.Where{ $_.ToLower() -eq "nonnodedata" }) + foreach ($node in $Script:ConfigurationDataContent.Keys.Where{ $_ -eq "NonNodeData" }) { $psd1Content += " @{`r`n" - $keyValuePair = $ConfigurationDataContent[$node].Entries + $keyValuePair = $Script:ConfigurationDataContent[$node].Entries foreach ($key in $keyValuePair.Keys | Sort-Object) { try @@ -1136,20 +1321,19 @@ hashtable for the ConfigurationData content as a formatted string. return $psd1Content } -function New-ConfigurationDataDocument -{ - <# +<# .SYNOPSIS -Generates a new ConfigurationData .psd1 file. + Generates a new ConfigurationData .psd1 file. .DESCRIPTION -This function will create the ConfigurationData .psd1 file and store -the content of the converted hashtable in it. + This function will create the ConfigurationData .psd1 file and store + the content of the converted hashtable in it. .PARAMETER Path -Full file path of the the resulting file will be located. - + Full file path where the resulting file will be located. #> +function New-ConfigurationDataDocument +{ [CmdletBinding()] param( [Parameter(Mandatory = $true)] @@ -1159,25 +1343,23 @@ Full file path of the the resulting file will be located. Get-ConfigurationDataContent | Out-File -FilePath $Path } -function ConvertTo-ConfigurationDataString -{ - <# +<# .SYNOPSIS -Converts items from the content of the dynamic hashtable to be used as -the content of the ConfigurationData .psd1 file into their proper string -representation. + Converts items from the content of the dynamic hashtable to their + proper string representation for the ConfigurationData .psd1 file. .DESCRIPTION -This function will loop through all items inside the dynamic hashtable -used for the resulting ConfigurationData .psd1 file's content and -converts each one to the proper string representation based on their -data type. + This function will loop through all items inside the dynamic hashtable + used for the resulting ConfigurationData .psd1 file's content and + converts each one to the proper string representation based on their + data type. .PARAMETER PSObject -The hashtable object we are building and which is to be used to drive -the content of the ConfigurationData .psd1 file. - + The hashtable object we are building and which is to be used to drive + the content of the ConfigurationData .psd1 file. #> +function ConvertTo-ConfigurationDataString +{ [CmdletBinding()] [OutputType([System.String])] param( @@ -1221,37 +1403,73 @@ the content of the ConfigurationData .psd1 file. return $configDataContent } -<# Region User based Methods #> -$Global:AllUsers = @() +$Script:AllUsers = @() -function Add-ReverseDSCUserName -{ - <# +<# .SYNOPSIS -Adds the provided username to the list of required users for the -destination environment. + Adds the provided username to the list of required users for the + destination environment. .DESCRIPTION -ReverseDSC allows you to keep track of all user credentials encountered -during various stages of the extraction process. By keeping a central list -of all users account required by the source environment we can easily -generate a script that will automatically create new user place holders -in a destination environment's Active Directory. This function checks -to see if the specified user was already encountered, and if not adds it -to the central list of all required users. + ReverseDSC allows you to keep track of all user credentials encountered + during various stages of the extraction process. By keeping a central + list of all user accounts required by the source environment we can + easily generate a script that will automatically create new user + placeholders in a destination environment's Active Directory. This + function checks to see if the specified user was already encountered, + and if not adds it to the central list of all required users. .PARAMETER UserName -Name of the user to add to the central list of required users. - + Name of the user to add to the central list of required users. #> +function Add-ReverseDSCUserName +{ [CmdletBinding()] param( [Parameter(Mandatory = $true)] [System.String] $UserName ) - if (-not $Global:AllUsers.Contains($UserName)) + if (-not $Script:AllUsers.Contains($UserName)) { - $Global:AllUsers += $UserName + $Script:AllUsers += $UserName } } + +<# +.SYNOPSIS + Retrieves the list of all user accounts required by the source + environment. + +.DESCRIPTION + This function returns the list of all user accounts that were + encountered during the extraction process and which are required for + the configuration to work in the destination environment. This list is + built by calling the Add-ReverseDSCUserName function every time a new + user account is encountered during the extraction. +#> +function Get-ReverseDSCUserNames +{ + [CmdletBinding()] + [OutputType([System.String[]])] + param() + return $Script:AllUsers +} + +<# +.SYNOPSIS + Clears the list of all user accounts required by the source environment. + +.DESCRIPTION + This function clears the list of all user accounts that were + encountered during the extraction process and which are required for + the configuration to work in the destination environment. This can be + useful to call at the beginning of an extraction to ensure that you are + starting with a clean slate in terms of required user accounts. +#> +function Clear-ReverseDSCUserNames +{ + [CmdletBinding()] + param() + $Script:AllUsers = @() +} diff --git a/ReverseDSC.psd1 b/ReverseDSC.psd1 index 176b789..123fb61 100644 --- a/ReverseDSC.psd1 +++ b/ReverseDSC.psd1 @@ -6,45 +6,47 @@ # Generated on: 2026/01/19 # @{ - ModuleVersion = '2.0.0.31' - GUID = '6c1176a0-4fac-4134-8ca2-3fa8a21a7b90' - Author = 'Microsoft Corporation' - CompanyName = 'Microsoft Corporation' - Copyright = '(c) 2015-2026 Microsoft Corporation. All rights reserved.' - Description = 'This DSC module is used to extract the DSC Configuration of existing environments.' - PowerShellVersion = '4.0' - NestedModules = @("ReverseDSC.Core.psm1") - CmdletsToExport = @() - FunctionsToExport = @("Get-DSCParamType", - "Get-DSCBlock", - "Get-DSCFakeParameters", - "Get-DSCDependsOnBlock", - "Export-TargetResource", - "Get-ResourceFriendlyName", - "Get-Credentials", - "Resolve-Credentials", - "Save-Credentials", - "Test-Credentials", - "Convert-DSCStringParamToVariable", - "Get-ConfigurationDataContent" - "New-ConfigurationDataDocument", - "Add-ConfigurationDataEntry", - "Get-ConfigurationDataEntry", - "Add-ReverseDSCUserName") - AliasesToExport = @() - PrivateData = @{ + ModuleVersion = '2.0.0.31' + GUID = '6c1176a0-4fac-4134-8ca2-3fa8a21a7b90' + Author = 'Microsoft Corporation' + CompanyName = 'Microsoft Corporation' + Copyright = '(c) 2015-2026 Microsoft Corporation. All rights reserved.' + Description = 'This DSC module is used to extract the DSC Configuration of existing environments.' + PowerShellVersion = '5.1' + NestedModules = @("ReverseDSC.Core.psm1") + CmdletsToExport = @() + FunctionsToExport = @( + "Clear-ConfigurationDataContent", + "Clear-ReverseDSCUserNames", + "Get-DSCParamType", + "Get-DSCBlock", + "Get-DSCFakeParameters", + "Get-DSCDependsOnBlock", + "Get-Credentials", + "Resolve-Credentials", + "Save-Credentials", + "Test-Credentials", + "Convert-DSCStringParamToVariable", + "Get-ConfigurationDataContent", + "New-ConfigurationDataDocument", + "Add-ConfigurationDataEntry", + "Get-ConfigurationDataEntry", + "Add-ReverseDSCUserName" + ) + AliasesToExport = @() + PrivateData = @{ PSData = @{ - Tags = @('DesiredStateConfiguration', 'DSC', 'DSCResourceKit', 'DSCResource', 'ReverseDSC') + Tags = @('DesiredStateConfiguration', 'DSC', 'DSCResourceKit', 'DSCResource', 'ReverseDSC') # A URL to the license for this module. - LicenseUri = '' + LicenseUri = 'https://github.com/microsoft/ReverseDSC/blob/master/LICENSE' # A URL to the main website for this project. - ProjectUri = 'https://Github.com/Microsoft/ReverseDSC' + ProjectUri = 'https://github.com/microsoft/ReverseDSC' # A URL to an icon representing this module. - IconUri = 'https://github.com/Microsoft/ReverseDSC/blob/master/Images/DSCModuleIcon.png?raw=true' + IconUri = 'https://github.com/microsoft/ReverseDSC/blob/master/Images/DSCModuleIcon.png?raw=true' ReleaseNotes = 'Add european localized quotation marks.' diff --git a/Tests/ReverseDSC.Core.Tests.ps1 b/Tests/ReverseDSC.Core.Tests.ps1 new file mode 100644 index 0000000..6451569 --- /dev/null +++ b/Tests/ReverseDSC.Core.Tests.ps1 @@ -0,0 +1,789 @@ +# Import module before tests +$modulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\ReverseDSC.Core.psm1' +Import-Module -Name $modulePath -Force + +InModuleScope 'ReverseDSC.Core' { + Describe 'ConvertTo-EscapedDSCString' { + Context 'When the input string is null or empty' { + It 'Should return the same empty string' { + $result = ConvertTo-EscapedDSCString -InputString '' + $result | Should -BeNullOrEmpty + } + + It 'Should return null when passed null' { + $result = ConvertTo-EscapedDSCString -InputString $null + $result | Should -BeNullOrEmpty + } + } + + Context 'When the input string contains backticks' { + It 'Should escape backticks by doubling them' { + $result = ConvertTo-EscapedDSCString -InputString 'Hello`World' + $result | Should -Be 'Hello``World' + } + } + + Context 'When the input string contains dollar signs' { + It 'Should escape dollar signs by default' { + $result = ConvertTo-EscapedDSCString -InputString 'Price is $100' + $result | Should -Be 'Price is `$100' + } + + It 'Should preserve dollar signs when AllowVariables is specified' { + $result = ConvertTo-EscapedDSCString -InputString 'Value is $var' -AllowVariables + $result | Should -Be 'Value is $var' + } + } + + Context 'When the input string contains European quotation marks' { + It 'Should escape U+201E (double low-9 quotation mark)' { + $input201E = "test$([char]0x201E)value" + $result = ConvertTo-EscapedDSCString -InputString $input201E + $result | Should -Be "test``$([char]0x201E)value" + } + + It 'Should escape U+201C (left double quotation mark)' { + $input201C = "test$([char]0x201C)value" + $result = ConvertTo-EscapedDSCString -InputString $input201C + $result | Should -Be "test``$([char]0x201C)value" + } + + It 'Should escape U+201D (right double quotation mark)' { + $input201D = "test$([char]0x201D)value" + $result = ConvertTo-EscapedDSCString -InputString $input201D + $result | Should -Be "test``$([char]0x201D)value" + } + } + + Context 'When the input string contains double quotes' { + It 'Should escape double quotes' { + $result = ConvertTo-EscapedDSCString -InputString 'She said "hello"' + $result | Should -Be 'She said `"hello`"' + } + } + + Context 'When the input string contains double quotes and escape characters' { + It 'Should escape double quotes and escape characters' { + $result = ConvertTo-EscapedDSCString -InputString 'She said "hello" with `"Escaped Text`"' + $result | Should -Be 'She said `"hello`" with ```"Escaped Text```"' + } + } + + Context 'When the input string is plain text without special characters' { + It 'Should return the string unchanged' { + $result = ConvertTo-EscapedDSCString -InputString 'Normal text' + $result | Should -Be 'Normal text' + } + } +} + +Describe 'ConvertTo-DSCStringValue' { + Context 'When the value is null' { + It 'Should return empty double-quoted string' { + $result = ConvertTo-DSCStringValue -Value $null + $result | Should -Be '""' + } + } + + Context 'When NoEscape is true' { + It 'Should return the raw value without escaping' { + $result = ConvertTo-DSCStringValue -Value 'MyValue' -NoEscape $true + $result | Should -Be 'MyValue' + } + } + + Context 'When NoEscape is false (default)' { + It 'Should return the value wrapped in double quotes' { + $result = ConvertTo-DSCStringValue -Value 'SimpleString' + $result | Should -Be '"SimpleString"' + } + + It 'Should escape special characters in the value' { + $result = ConvertTo-DSCStringValue -Value 'Value with $var' + $result | Should -Be '"Value with `$var"' + } + } + + Context 'When AllowVariables is true' { + It 'Should preserve dollar signs in the value' { + $result = ConvertTo-DSCStringValue -Value 'Value with $var' -AllowVariables $true + $result | Should -Be '"Value with $var"' + } + } +} + +Describe 'ConvertTo-DSCBooleanValue' { + It 'Should return $True for true values' { + $result = ConvertTo-DSCBooleanValue -Value $true + $result | Should -Be '$True' + } + + It 'Should return $False for false values' { + $result = ConvertTo-DSCBooleanValue -Value $false + $result | Should -Be '$False' + } +} + +Describe 'ConvertTo-DSCCredentialValue' { + Context 'When the value is null' { + It 'Should return a Get-Credential command with the parameter name' { + $result = ConvertTo-DSCCredentialValue -Value $null -ParameterName 'Credential' + $result | Should -Be 'Get-Credential -Message Credential' + } + } + + Context 'When the value is a PSCredential with a UPN username' { + BeforeAll { + $securePassword = ConvertTo-SecureString -String 'Password123' -AsPlainText -Force + $credential = New-Object System.Management.Automation.PSCredential ('admin@contoso.com', $securePassword) + } + + It 'Should return a $Creds variable based on the username part' { + $result = ConvertTo-DSCCredentialValue -Value $credential -ParameterName 'Credential' + $result | Should -Be '$Credsadmin' + } + } + + Context 'When the value is a PSCredential with a domain\user username' { + BeforeAll { + $securePassword = ConvertTo-SecureString -String 'Password123' -AsPlainText -Force + $credential = New-Object System.Management.Automation.PSCredential ('CONTOSO\admin', $securePassword) + } + + It 'Should return a $Creds variable based on the username after backslash' { + $result = ConvertTo-DSCCredentialValue -Value $credential -ParameterName 'Credential' + $result | Should -Be '$Credsadmin' + } + } + + Context 'When the value is a PSCredential with special characters in username' { + BeforeAll { + $securePassword = ConvertTo-SecureString -String 'Password123' -AsPlainText -Force + $credential = New-Object System.Management.Automation.PSCredential ('CONTOSO\admin-user.name', $securePassword) + } + + It 'Should sanitize special characters in the variable name' { + $result = ConvertTo-DSCCredentialValue -Value $credential -ParameterName 'Credential' + $result | Should -Be '$Credsadmin_user_name' + } + } +} + +Describe 'ConvertTo-DSCHashtableValue' { + It 'Should format a single-entry hashtable correctly' { + $hashtable = @{ Key1 = 'Value1' } + $result = ConvertTo-DSCHashtableValue -Value $hashtable + $result | Should -BeLike '@{*Key1 = "Value1"*}' + } + + It 'Should format a multi-entry hashtable correctly' { + $hashtable = [ordered]@{ Key1 = 'Value1'; Key2 = 'Value2' } + $result = ConvertTo-DSCHashtableValue -Value $hashtable + $result | Should -BeLike '@{Key1*Key2*}' + $result | Should -Match 'Key1 = "Value1"' + $result | Should -Match 'Key2 = "Value2"' + } + + It 'Should wrap the result in @{ }' { + $hashtable = @{ A = 'B' } + $result = ConvertTo-DSCHashtableValue -Value $hashtable + $result | Should -Match '^@\{' + $result | Should -Match '\}$' + } +} + +Describe 'ConvertTo-DSCStringArrayValue' { + Context 'When the value is null or empty' { + It 'Should return @() for null value' { + $result = ConvertTo-DSCStringArrayValue -Value $null + $result | Should -Be '@()' + } + + It 'Should return @() for empty array' { + $result = ConvertTo-DSCStringArrayValue -Value @() + $result | Should -Be '@()' + } + } + + Context 'When the value is a single-element array' { + It 'Should return a properly formatted array string' { + $result = ConvertTo-DSCStringArrayValue -Value @('Item1') + $result | Should -Be '@("Item1")' + } + } + + Context 'When the value is a multi-element array' { + It 'Should return a comma-separated array string' { + $result = ConvertTo-DSCStringArrayValue -Value @('Item1', 'Item2', 'Item3') + $result | Should -Be '@("Item1","Item2","Item3")' + } + } + + Context 'When NoEscape is true' { + It 'Should not escape special characters in array elements' { + $result = ConvertTo-DSCStringArrayValue -Value @('$var1', '$var2') -NoEscape $true + $result | Should -Be '@("$var1","$var2")' + } + } +} + +Describe 'ConvertTo-DSCIntegerArrayValue' { + Context 'When the value is null or empty' { + It 'Should return @() for null value' { + $result = ConvertTo-DSCIntegerArrayValue -Value $null + $result | Should -Be '@()' + } + + It 'Should return @() for empty array' { + $result = ConvertTo-DSCIntegerArrayValue -Value @() + $result | Should -Be '@()' + } + } + + Context 'When the value contains integers' { + It 'Should return a comma-separated integer array' { + $result = ConvertTo-DSCIntegerArrayValue -Value @(1, 2, 3) + $result | Should -Be '@(1,2,3)' + } + + It 'Should handle a single integer' { + $result = ConvertTo-DSCIntegerArrayValue -Value @(42) + $result | Should -Be '@(42)' + } + } +} + +Describe 'ConvertTo-DSCObjectArrayValue' { + Context 'When the value is null or empty' { + It 'Should return @() for null value' { + $result = ConvertTo-DSCObjectArrayValue -Value $null + $result | Should -Be '@()' + } + + It 'Should return @() for empty array' { + $result = ConvertTo-DSCObjectArrayValue -Value @() + $result | Should -Be '@()' + } + } + + Context 'When the value contains strings' { + It 'Should format string elements with quotes' { + $result = ConvertTo-DSCObjectArrayValue -Value @('A', 'B', 'C') + $result | Should -Be '@("A","B","C")' + } + } + + Context 'When the value contains hashtables' { + It 'Should format each hashtable in the array' { + $value = @( + @{ Name = 'Item1' } + ) + $result = ConvertTo-DSCObjectArrayValue -Value $value + $result | Should -BeLike '@(@{*Name*Item1*})' + } + + It 'Should handle null values in hashtable entries' { + $value = @( + @{ Name = $null } + ) + $result = ConvertTo-DSCObjectArrayValue -Value $value + $result | Should -Match '\$null' + } + + It 'Should handle array values in hashtable entries' { + $value = @( + @{ Items = @('A', 'B') } + ) + $result = ConvertTo-DSCObjectArrayValue -Value $value + $result | Should -Match "@\(" + } + } + + Context 'When NoEscape is true' { + It 'Should not escape string values' { + $result = ConvertTo-DSCObjectArrayValue -Value @('$var') -NoEscape $true + $result | Should -Match '\$var' + } + } +} + +Describe 'Get-DSCDependsOnBlock' { + It 'Should generate a proper DependsOn clause for a single dependency' { + $result = Get-DSCDependsOnBlock -DependsOnItems @('[xWebsite]DefaultSite') + $result | Should -Be '@("[xWebsite]DefaultSite");' + } + + It 'Should generate a proper DependsOn clause for multiple dependencies' { + $result = Get-DSCDependsOnBlock -DependsOnItems @('[xWebsite]DefaultSite', '[xSPSite]MainSite') + $result | Should -Be '@("[xWebsite]DefaultSite","[xSPSite]MainSite");' + } +} + +Describe 'Save-Credentials' { + BeforeEach { + # Reset the credentials repo before each test + $Script:CredsRepo = @() + } + + It 'Should add a new username to the credentials repository' { + Save-Credentials -UserName 'CONTOSO\admin' + $Script:CredsRepo | Should -Contain 'contoso\admin' + } + + It 'Should store usernames in lowercase' { + Save-Credentials -UserName 'CONTOSO\ADMIN' + $Script:CredsRepo | Should -Contain 'contoso\admin' + } + + It 'Should not duplicate usernames' { + Save-Credentials -UserName 'CONTOSO\admin' + Save-Credentials -UserName 'contoso\admin' + $Script:CredsRepo | Should -HaveCount 1 + } +} + +Describe 'Get-Credentials' { + BeforeAll { + $Script:CredsRepo = @() + Save-Credentials -UserName 'CONTOSO\admin' + } + + It 'Should return the username when it exists in the repository' { + $result = Get-Credentials -UserName 'CONTOSO\admin' + $result | Should -Be 'contoso\admin' + } + + It 'Should return null when the username is not in the repository' { + $result = Get-Credentials -UserName 'CONTOSO\nonexistent' + $result | Should -BeNullOrEmpty + } +} + +Describe 'Test-Credentials' { + BeforeAll { + $Script:CredsRepo = @() + Save-Credentials -UserName 'CONTOSO\admin' + } + + It 'Should return true when the username exists' { + $result = Test-Credentials -UserName 'CONTOSO\admin' + $result | Should -BeTrue + } + + It 'Should return false when the username does not exist' { + $result = Test-Credentials -UserName 'CONTOSO\unknown' + $result | Should -BeFalse + } +} + +Describe 'Resolve-Credentials' { + It 'Should return $Creds for domain\user format' { + $result = Resolve-Credentials -UserName 'CONTOSO\admin' + $result | Should -Be '$Credsadmin' + } + + It 'Should sanitize hyphens to underscores' { + $result = Resolve-Credentials -UserName 'CONTOSO\admin-user' + $result | Should -Be '$Credsadmin_user' + } + + It 'Should sanitize dots to underscores' { + $result = Resolve-Credentials -UserName 'CONTOSO\admin.user' + $result | Should -Be '$Credsadmin_user' + } + + It 'Should remove spaces and @ signs' { + $result = Resolve-Credentials -UserName 'admin @company' + $result | Should -Be '$Credsadmincompany' + } + + It 'Should handle a simple username without domain' { + $result = Resolve-Credentials -UserName 'admin' + $result | Should -Be '$Credsadmin' + } +} + +Describe 'Add-ReverseDSCUserName' { + BeforeEach { + $Script:AllUsers = @() + } + + It 'Should add a username to the list' { + Add-ReverseDSCUserName -UserName 'user1@contoso.com' + $Script:AllUsers | Should -Contain 'user1@contoso.com' + } + + It 'Should not add duplicate usernames' { + Add-ReverseDSCUserName -UserName 'user1@contoso.com' + Add-ReverseDSCUserName -UserName 'user1@contoso.com' + $Script:AllUsers | Should -HaveCount 1 + } +} + +Describe 'Get-ReverseDSCUserNames' { + BeforeAll { + $Script:AllUsers = @() + Add-ReverseDSCUserName -UserName 'user1@contoso.com' + Add-ReverseDSCUserName -UserName 'user2@contoso.com' + } + + It 'Should return all added usernames' { + $result = Get-ReverseDSCUserNames + $result | Should -HaveCount 2 + $result | Should -Contain 'user1@contoso.com' + $result | Should -Contain 'user2@contoso.com' + } +} + +Describe 'Clear-ReverseDSCUserNames' { + BeforeAll { + Add-ReverseDSCUserName -UserName 'user1@contoso.com' + } + + It 'Should clear all usernames from the list' { + Clear-ReverseDSCUserNames + $Script:AllUsers | Should -HaveCount 0 + } +} + +Describe 'Add-ConfigurationDataEntry' { + BeforeEach { + Clear-ConfigurationDataContent + } + + It 'Should add an entry under a new node' { + Add-ConfigurationDataEntry -Node 'localhost' -Key 'Setting1' -Value 'Value1' + $result = Get-ConfigurationDataEntry -Node 'localhost' -Key 'Setting1' + $result.Value | Should -Be 'Value1' + } + + It 'Should add an entry with a description' { + Add-ConfigurationDataEntry -Node 'localhost' -Key 'Setting1' -Value 'Value1' -Description 'Test setting' + $result = Get-ConfigurationDataEntry -Node 'localhost' -Key 'Setting1' + $result.Value | Should -Be 'Value1' + $result.Description | Should -Be 'Test setting' + } + + It 'Should update the value when adding the same key to the same node' { + Add-ConfigurationDataEntry -Node 'localhost' -Key 'Setting1' -Value 'Value1' + Add-ConfigurationDataEntry -Node 'localhost' -Key 'Setting1' -Value 'Value2' + $result = Get-ConfigurationDataEntry -Node 'localhost' -Key 'Setting1' + $result.Value | Should -Be 'Value2' + } + + It 'Should support multiple nodes' { + Add-ConfigurationDataEntry -Node 'Server1' -Key 'Key1' -Value 'A' + Add-ConfigurationDataEntry -Node 'Server2' -Key 'Key1' -Value 'B' + (Get-ConfigurationDataEntry -Node 'Server1' -Key 'Key1').Value | Should -Be 'A' + (Get-ConfigurationDataEntry -Node 'Server2' -Key 'Key1').Value | Should -Be 'B' + } +} + +Describe 'Get-ConfigurationDataEntry' { + BeforeAll { + Clear-ConfigurationDataContent + Add-ConfigurationDataEntry -Node 'localhost' -Key 'TestKey' -Value 'TestValue' + } + + It 'Should return the entry for a specific node and key' { + $result = Get-ConfigurationDataEntry -Node 'localhost' -Key 'TestKey' + $result | Should -Not -BeNullOrEmpty + $result.Value | Should -Be 'TestValue' + } + + It 'Should return null when the key does not exist' { + $result = Get-ConfigurationDataEntry -Node 'localhost' -Key 'NonExistent' + $result | Should -BeNullOrEmpty + } +} + +Describe 'Clear-ConfigurationDataContent' { + It 'Should clear all configuration data entries' { + Add-ConfigurationDataEntry -Node 'localhost' -Key 'TestKey' -Value 'TestValue' + Clear-ConfigurationDataContent + $result = Get-ConfigurationDataEntry -Node 'localhost' -Key 'TestKey' + $result | Should -BeNullOrEmpty + } +} + +Describe 'Get-ConfigurationDataContent' { + BeforeAll { + Clear-ConfigurationDataContent + Add-ConfigurationDataEntry -Node 'localhost' -Key 'ServerName' -Value 'MyServer' -Description 'The server name' + } + + It 'Should return a string containing the AllNodes section' { + $result = Get-ConfigurationDataContent + $result | Should -Match 'AllNodes' + } + + It 'Should include the node name' { + $result = Get-ConfigurationDataContent + $result | Should -Match 'localhost' + } + + It 'Should include the key and value' { + $result = Get-ConfigurationDataContent + $result | Should -Match 'ServerName' + $result | Should -Match 'MyServer' + } + + It 'Should include the description as a comment' { + $result = Get-ConfigurationDataContent + $result | Should -Match '# The server name' + } + + It 'Should include NonNodeData section' { + $result = Get-ConfigurationDataContent + $result | Should -Match 'NonNodeData' + } + + It 'Should start with @{ and end with }' { + $result = Get-ConfigurationDataContent + $result | Should -Match '^@\{' + $result | Should -Match '\}$' + } +} + +Describe 'New-ConfigurationDataDocument' { + BeforeAll { + Clear-ConfigurationDataContent + Add-ConfigurationDataEntry -Node 'localhost' -Key 'TestKey' -Value 'TestValue' + $testPath = Join-Path -Path $TestDrive -ChildPath 'TestConfig.psd1' + } + + It 'Should create a .psd1 file at the specified path' { + New-ConfigurationDataDocument -Path $testPath + Test-Path -Path $testPath | Should -BeTrue + } + + It 'Should write valid content to the file' { + New-ConfigurationDataDocument -Path $testPath + $content = Get-Content -Path $testPath -Raw + $content | Should -Match 'AllNodes' + $content | Should -Match 'TestKey' + } +} + +Describe 'ConvertTo-ConfigurationDataString' { + Context 'When converting a string object' { + It 'Should wrap the string in quotes with a semicolon' { + $result = ConvertTo-ConfigurationDataString -PSObject 'TestValue' + $result | Should -Match '"TestValue"' + } + } + + Context 'When converting an array of strings' { + It 'Should format as a PowerShell array block' { + $result = ConvertTo-ConfigurationDataString -PSObject @('Item1', 'Item2') + $result | Should -Match '@\(' + $result | Should -Match 'Item1' + $result | Should -Match 'Item2' + } + } + + Context 'When converting a hashtable' { + It 'Should format as a PowerShell hashtable block' { + $hashtable = @{ Name = 'Test' } + $result = ConvertTo-ConfigurationDataString -PSObject $hashtable + $result | Should -Match '@\{' + $result | Should -Match 'Name' + } + } +} + +Describe 'Convert-DSCStringParamToVariable' { + Context 'When converting a simple string parameter to a variable' { + It 'Should remove quotes around the parameter value' { + $dscBlock = " ParamName = `"SomeValue`";`r`n" + $result = Convert-DSCStringParamToVariable -DSCBlock $dscBlock -ParameterName 'ParamName' + $result | Should -Not -Match '"SomeValue"' + $result | Should -Match 'SomeValue' + } + } + + Context 'When the parameter name is not found' { + It 'Should return the original DSCBlock unchanged' { + $dscBlock = " OtherParam = `"Value`";`r`n" + $result = Convert-DSCStringParamToVariable -DSCBlock $dscBlock -ParameterName 'NonExistent' + $result | Should -Be $dscBlock + } + } +} + +Describe 'Get-DSCBlock' { + BeforeAll { + # Create a minimal DSC resource module for testing + $testModulePath = Join-Path -Path $TestDrive -ChildPath 'TestResource.psm1' + $moduleContent = @' +function Get-TargetResource +{ + param( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.Boolean] + $Enabled, + + [Parameter()] + [System.String[]] + $Items + ) +} + +function Set-TargetResource +{ + param( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.Boolean] + $Enabled, + + [Parameter()] + [System.String[]] + $Items + ) +} +'@ + Set-Content -Path $testModulePath -Value $moduleContent + } + + Context 'When generating a DSC block with string parameters' { + It 'Should produce a properly formatted DSC configuration block' { + $params = @{ + Name = 'TestResource' + } + $result = Get-DSCBlock -ModulePath $testModulePath -Params $params + $result | Should -Not -BeNullOrEmpty + $result | Should -Match 'Name' + $result | Should -Match 'TestResource' + } + } + + Context 'When generating a DSC block with boolean parameters' { + It 'Should format boolean values with $ prefix' { + $params = @{ + Name = 'Test' + Enabled = $true + } + $result = Get-DSCBlock -ModulePath $testModulePath -Params $params + $result | Should -Match '\$True' + } + } + + Context 'When generating a DSC block with string array parameters' { + It 'Should format string arrays with @()' { + $params = @{ + Name = 'Test' + Items = @('Item1', 'Item2') + } + $result = Get-DSCBlock -ModulePath $testModulePath -Params $params + $result | Should -Match '@\(' + $result | Should -Match 'Item1' + $result | Should -Match 'Item2' + } + } + + Context 'When parameters are aligned' { + It 'Should pad shorter parameter names with spaces for alignment' { + $params = @{ + Name = 'Test' + Enabled = $true + } + $result = Get-DSCBlock -ModulePath $testModulePath -Params $params + # Both parameters should have equal signs, and shorter names should have more spacing + $result | Should -Match 'Name\s+=' + $result | Should -Match 'Enabled\s+=' + } + } + + Context 'When _metadata_ properties are present' { + It 'Should exclude _metadata_ keys from the output but include their values as comments' { + $params = @{ + Name = 'Test' + _metadata_Name = '# This is a comment' + } + $result = Get-DSCBlock -ModulePath $testModulePath -Params $params + $result | Should -Not -Match '_metadata_' + $result | Should -Match '# This is a comment' + } + } + + Context 'When null values are present' { + It 'Should exclude parameters with null values' { + $params = @{ + Name = 'Test' + Items = $null + } + $result = Get-DSCBlock -ModulePath $testModulePath -Params $params + # Null params are excluded in the preprocessing step + $result | Should -Match 'Name' + } + } + + Context 'When NoEscape is specified for a parameter' { + It 'Should not escape the specified parameter values' { + $params = @{ + Name = '$ConfigName' + } + $result = Get-DSCBlock -ModulePath $testModulePath -Params $params -NoEscape @('Name') + $result | Should -Match '\$ConfigName' + $result | Should -Not -Match '`\$ConfigName' + } + } + + Context 'When hashtable parameters are provided' { + It 'Should format hashtable values as @{ key = value }' { + $params = @{ + Name = 'Test' + Items = @{ SubKey = 'SubValue' } + } + $result = Get-DSCBlock -ModulePath $testModulePath -Params $params + $result | Should -Match '@\{' + $result | Should -Match 'SubKey' + } + } +} + +Describe 'Module Exports' { + BeforeAll { + $manifestPath = Join-Path -Path $PSScriptRoot -ChildPath '..\ReverseDSC.psd1' + $manifest = Test-ModuleManifest -Path $manifestPath -ErrorAction SilentlyContinue + } + + It 'Should have a valid module manifest' { + $manifest | Should -Not -BeNullOrEmpty + } + + It 'Should export expected functions' -ForEach @( + @{ FunctionName = 'ConvertTo-EscapedDSCString' } + @{ FunctionName = 'Get-DSCParamType' } + @{ FunctionName = 'Get-DSCBlock' } + @{ FunctionName = 'Get-DSCFakeParameters' } + @{ FunctionName = 'Get-DSCDependsOnBlock' } + @{ FunctionName = 'Get-Credentials' } + @{ FunctionName = 'Resolve-Credentials' } + @{ FunctionName = 'Save-Credentials' } + @{ FunctionName = 'Test-Credentials' } + @{ FunctionName = 'Convert-DSCStringParamToVariable' } + @{ FunctionName = 'Get-ConfigurationDataContent' } + @{ FunctionName = 'New-ConfigurationDataDocument' } + @{ FunctionName = 'Add-ConfigurationDataEntry' } + @{ FunctionName = 'Get-ConfigurationDataEntry' } + @{ FunctionName = 'Clear-ConfigurationDataContent' } + @{ FunctionName = 'Add-ReverseDSCUserName' } + ) { + Get-Command -Name $FunctionName -Module 'ReverseDSC.Core' -ErrorAction SilentlyContinue | + Should -Not -BeNullOrEmpty + } +} + +} # InModuleScope + +# Cleanup +Remove-Module -Name 'ReverseDSC.Core' -ErrorAction SilentlyContinue From 4c6a121744e378ac3dc6835aca56fd1e98fe48af Mon Sep 17 00:00:00 2001 From: Fabien Tschanz Date: Tue, 17 Feb 2026 10:32:15 +0100 Subject: [PATCH 2/2] Fix double escape backtick in literal string --- ReverseDSC.Core.psm1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ReverseDSC.Core.psm1 b/ReverseDSC.Core.psm1 index ecf7bb2..96cc1a8 100644 --- a/ReverseDSC.Core.psm1 +++ b/ReverseDSC.Core.psm1 @@ -584,7 +584,7 @@ function Get-DSCBlock { $value = ConvertTo-DSCHashtableValue -Value $paramValue } - '^(System\.String\[\]|String\[\]|ArrayList|List``1)$' + '^(System\.String\[\]|String\[\]|ArrayList|List`1)$' { if ($paramValue.ToString().StartsWith("`$ConfigurationData.")) {