diff --git a/CHANGELOG.md b/CHANGELOG.md index aaebc9e..fd19cb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # IT-ToolBox - Change History +## 3.2.0 - 2026-10-24 + +- Port Test-IsRegistryKey from PowerShell-Functions, bringing the supported API to 31 commands. +- Share a private registry-key opener with Test-RegistryValue to preserve literal paths, missing-key handling and handle disposal. +- Keep the standalone KeyPath parameter and Boolean contract; reject unsupported platforms and propagate provider/permission errors. +- Update command inventory, release metadata and archive/import expectations; add offline and Windows registry regression tests. + ## 3.1.0 - 2026-10-24 - Add Test-IsGuid from PowerShell-Functions as a supported dependency-free validator, bringing the exported API to 30 commands. diff --git a/IT-ToolBox.psd1 b/IT-ToolBox.psd1 index b4a937e..eca9087 100644 --- a/IT-ToolBox.psd1 +++ b/IT-ToolBox.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'IT-ToolBox.psm1' - ModuleVersion = '3.1.0' + ModuleVersion = '3.2.0' GUID = '02c0c748-00ba-4e8f-b8ce-6aa7e4cefe08' Author = 'Daniele Catanesi - SingularVector' CompanyName = 'Community' @@ -26,6 +26,7 @@ 'Convert-LogonTimestamp' 'Get-OsUpTime' 'Remove-SpecialCharacters' + 'Test-IsRegistryKey' 'Test-RegistryValue' 'Test-IsValidDn' 'Test-IsValidUpn' @@ -45,7 +46,7 @@ AliasesToExport = @() PrivateData = @{ PSData = @{ - ReleaseNotes = '3.1.0: Add dependency-free Test-IsGuid syntax validation with pipeline support. Includes 3.0.1 logger fixes; exports 30 commands. See CHANGELOG.md.' + ReleaseNotes = '3.2.0: Add Windows-only Test-IsRegistryKey; share registry path validation and key access with Test-RegistryValue. Exports 31 commands with no new dependencies. See CHANGELOG.md.' Tags = @('Infrastructure', 'Automation', 'Logging') LicenseUri = 'https://github.com/SingularVector/IT-ToolBox/blob/master/LICENSE' ProjectUri = 'https://github.com/SingularVector/IT-ToolBox' diff --git a/IT-ToolBox.psm1 b/IT-ToolBox.psm1 index 5e7a5d7..6824170 100644 --- a/IT-ToolBox.psm1 +++ b/IT-ToolBox.psm1 @@ -26,6 +26,7 @@ Export-ModuleMember -Function @( 'Convert-LogonTimestamp' 'Get-OsUpTime' 'Remove-SpecialCharacters' + 'Test-IsRegistryKey' 'Test-RegistryValue' 'Test-IsValidDn' 'Test-IsValidUpn' diff --git a/Private/Open-ITToolBoxRegistryKey.ps1 b/Private/Open-ITToolBoxRegistryKey.ps1 new file mode 100644 index 0000000..8211cca --- /dev/null +++ b/Private/Open-ITToolBoxRegistryKey.ps1 @@ -0,0 +1,25 @@ +function Open-ITToolBoxRegistryKey +{ + [CmdletBinding()] + param ([Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$Path) + + if (-not $IsWindows) + { + throw [PlatformNotSupportedException]::new('Registry functions require the Windows Registry provider.') + } + $provider = $null + $drive = $null + [void]$ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path, [ref]$provider, [ref]$drive) + if ($provider.Name -ne 'Registry') + { + throw 'Path must use the Registry provider.' + } + try + { + return Get-Item -LiteralPath $Path -ErrorAction Stop + } + catch [System.Management.Automation.ItemNotFoundException] + { + return $null + } +} diff --git a/Public/Test-IsRegistryKey.ps1 b/Public/Test-IsRegistryKey.ps1 new file mode 100644 index 0000000..04b006a --- /dev/null +++ b/Public/Test-IsRegistryKey.ps1 @@ -0,0 +1,18 @@ +function Test-IsRegistryKey +{ + <# + .SYNOPSIS + Tests whether a literal Windows Registry key exists. + .DESCRIPTION + Missing keys return false. Provider, permission and operational errors + terminate. Non-Windows systems are unsupported. This tests keys, not values. + #> + [CmdletBinding()] + [OutputType([bool])] + param ([Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$KeyPath) + + $key = Open-ITToolBoxRegistryKey -Path $KeyPath + if ($null -eq $key) { return $false } + try { return $true } + finally { $key.Dispose() } +} diff --git a/Public/Test-RegistryValue.ps1 b/Public/Test-RegistryValue.ps1 index c60cec4..63c6230 100644 --- a/Public/Test-RegistryValue.ps1 +++ b/Public/Test-RegistryValue.ps1 @@ -30,27 +30,8 @@ function Test-RegistryValue [string]$Value ) - if (-not $IsWindows) - { - throw [PlatformNotSupportedException]::new('Test-RegistryValue requires the Windows Registry provider.') - } - - [System.Management.Automation.ProviderInfo]$provider = $null - [System.Management.Automation.PSDriveInfo]$drive = $null - - # Resolve the path to confirm that the supplied path is actually using the Registry provider. - [void]$ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path, [ref]$provider, [ref]$drive) - - if ($provider.Name -ne 'Registry') - { - throw 'Path must use the Registry provider.' - } - - try - { - $key = Get-Item -LiteralPath $Path -ErrorAction Stop - } - catch [System.Management.Automation.ItemNotFoundException] + $key = Open-ITToolBoxRegistryKey -Path $Path + if ($null -eq $key) { return $false } diff --git a/README.md b/README.md index 4d17758..d55a899 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ [![PowerShell 7.4+](https://img.shields.io/badge/PowerShell-7.4%2B-5391FE?logo=powershell)](https://github.com/PowerShell/PowerShell) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](./LICENSE) -[![Version: 3.1.0](https://img.shields.io/badge/version-3.1.0-blue.svg)](./CHANGELOG.md) +[![Version: 3.2.0](https://img.shields.io/badge/version-3.2.0-blue.svg)](./CHANGELOG.md) [![Testing wanted](https://img.shields.io/badge/testing-wanted-yellow.svg)](https://github.com/SingularVector/IT-ToolBox/issues/new/choose) -> **Testing wanted — help validate IT-ToolBox 3.1.0 in real environments.** +> **Testing wanted — help validate IT-ToolBox 3.2.0 in real environments.** > Feedback is especially useful for Windows AD report chains, remote CIM uptime, > API authentication and platform clipboard behavior. > [Report a bug](https://github.com/SingularVector/IT-ToolBox/issues/new/choose) @@ -38,17 +38,17 @@ and remote CIM commands have the platform requirements listed below. ## Release status -**IT-ToolBox 3.1.0** contains the 30-command API documented below, including GUID -syntax validation and the logger fixes from 3.0.1. The manifest, loader, +**IT-ToolBox 3.2.0** contains the 31-command API documented below, including registry +key existence checks, GUID syntax validation and the logger fixes from 3.0.1. The manifest, loader, command inventory and installable archive use the same supported function set. -See the [3.1.0 release notes](./CHANGELOG.md) for the changes and migration details. +See the [3.2.0 release notes](./CHANGELOG.md) for the changes and migration details. -Release builds should use the reviewed 3.1.0 commit or tag rather than a moving +Release builds should use the reviewed 3.2.0 commit or tag rather than a moving master checkout. Build and installation instructions are in [Packaging](./docs/Packaging.md). ## Status and requirements -The module has **30 exported functions**, matching the manifest and loader. +The module has **31 exported functions**, matching the manifest and loader. Automated tests cover the supported API; live AD, remote CIM and service authentication remain unverified. Requires PowerShell 7.4 or later (Core edition). Windows @@ -56,7 +56,7 @@ PowerShell 5.1 is not supported. The module imports without WinSCP, GnuPG, Active Directory or Exchange dependencies. Most utilities run on Windows, Linux and macOS. -`Test-RegistryValue` is Windows-only; remote `Get-OsUpTime` requires Windows CIM +`Test-IsRegistryKey` and `Test-RegistryValue` are Windows-only; remote `Get-OsUpTime` requires Windows CIM cmdlets and a reachable Windows endpoint. `Get-ReportChain` requires an available `Get-ADUser` command and access to AD when invoked. Clipboard output requires a working platform clipboard backend. @@ -139,6 +139,7 @@ parameter help and the sections below for compatibility details. | `Convert-LogonTimestamp` | Convert a supplied FILETIME; zero produces no date. | | `Get-OsUpTime` | Read local uptime or query Windows uptime through CIM. | | `Remove-SpecialCharacters` | Preview or rename filesystem items using the original punctuation policy. | +| `Test-IsRegistryKey` | Check whether a literal registry key exists on Windows. | | `Test-RegistryValue` | Check a literal registry value name on Windows. | @@ -215,7 +216,7 @@ elapsed duration expressed in hours. These switches are mutually exclusive. - All commands formerly retained in `Staging/v3/` now have supported implementations. Its README records the migration. Exchange/AzureAD session wrappers and the global certificate-validation bypass have been removed; their source remains in Git history. -- Only the thirty listed commands are exported. Private helpers, variables and aliases +- Only the thirty-one listed commands are exported. Private helpers, variables and aliases are not exported. Do not treat v3 as a drop-in replacement for every v2 script: removed commands and documented parameter, output and encryption changes require migration. No restoration of removed service wrappers is promised. @@ -481,6 +482,13 @@ are logged; WhatIf suppresses logging. This replaces the staged implementation's silent default output, fixed C:\Temp logging and swallowed errors. +`Test-IsRegistryKey -KeyPath 'HKCU:\Software\Example'` checks the key itself, +even when it contains no values. It returns true for an existing key and false for +a missing key. Wildcard characters are literal. Permission and provider errors +terminate rather than being treated as absence. The command is Windows-only, +uses the current process's registry view and does not inspect remote registries. +The same behavior is available in the standalone PowerShell-Functions repository. + `Test-RegistryValue -Path 'HKCU:\Software\Example' -Value 'Enabled'` is Windows-only. It checks literal value names, case-insensitively, and returns true for existing empty strings or zero data. Use `-Value ''` for an unnamed/default value. Missing diff --git a/Tests/IntegrationBoundary.Tests.ps1 b/Tests/IntegrationBoundary.Tests.ps1 index fc3a426..e0d09b5 100644 --- a/Tests/IntegrationBoundary.Tests.ps1 +++ b/Tests/IntegrationBoundary.Tests.ps1 @@ -67,7 +67,7 @@ finally { $result.ErrorPreference | Should -Be 'Continue' $result.WarningPreference | Should -Be 'Continue' $result.LocationPreserved | Should -BeTrue - $result.ExportCount | Should -Be 30 + $result.ExportCount | Should -Be 31 $result.HistoricalExports | Should -Be 0 } } diff --git a/Tests/Module.Tests.ps1 b/Tests/Module.Tests.ps1 index 3b41c75..8499e9f 100644 --- a/Tests/Module.Tests.ps1 +++ b/Tests/Module.Tests.ps1 @@ -6,11 +6,11 @@ BeforeAll { Describe 'IT-ToolBox module boundary' { It 'has a valid manifest' { $manifest = Test-ModuleManifest $manifestPath -ErrorAction Stop - $manifest.Version | Should -Be ([version]'3.1.0') + $manifest.Version | Should -Be ([version]'3.2.0') } It 'exports exactly the supported commands' { - $expected = @('New-LogEntry', 'New-Timer', 'Get-TimerStatus', 'Stop-Timer', 'Get-ElapsedTime', 'Get-ScriptDirectory', 'Get-ScriptName', 'Test-FileName', 'Test-IsValidPath', 'Test-IsIP', 'Test-IsGuid', 'Test-IsDate', 'Test-IsEmail', 'Test-IsUrl', 'Convert-LogonTimestamp', 'Get-OsUpTime', 'Remove-SpecialCharacters', 'Test-RegistryValue', 'Test-IsValidDn', 'Test-IsValidUpn', 'Get-ReportChain', 'New-StringEncryption', 'New-StringDecryption', 'New-RandomString', 'New-RandomPassword', 'New-PhoneticPassword', 'New-ApiRequest', 'New-StringConversion', 'Get-StringCheckSum', 'Get-StringHashCode') | Sort-Object + $expected = @('New-LogEntry', 'New-Timer', 'Get-TimerStatus', 'Stop-Timer', 'Get-ElapsedTime', 'Get-ScriptDirectory', 'Get-ScriptName', 'Test-FileName', 'Test-IsValidPath', 'Test-IsIP', 'Test-IsGuid', 'Test-IsDate', 'Test-IsEmail', 'Test-IsUrl', 'Convert-LogonTimestamp', 'Get-OsUpTime', 'Remove-SpecialCharacters', 'Test-IsRegistryKey', 'Test-RegistryValue', 'Test-IsValidDn', 'Test-IsValidUpn', 'Get-ReportChain', 'New-StringEncryption', 'New-StringDecryption', 'New-RandomString', 'New-RandomPassword', 'New-PhoneticPassword', 'New-ApiRequest', 'New-StringConversion', 'Get-StringCheckSum', 'Get-StringHashCode') | Sort-Object $actual = @($module.ExportedFunctions.Keys | Sort-Object) ($actual -join ',') | Should -Be ($expected -join ',') $module.ExportedVariables.Count | Should -Be 0 @@ -19,6 +19,7 @@ Describe 'IT-ToolBox module boundary' { } It 'keeps logging helpers private' { + Get-Command Open-ITToolBoxRegistryKey -Module IT-ToolBox -ErrorAction SilentlyContinue | Should -BeNullOrEmpty Get-Command Get-NewLogEntryMutexName -Module IT-ToolBox -ErrorAction SilentlyContinue | Should -BeNullOrEmpty Get-Command Test-ITToolBoxDnsName -Module IT-ToolBox -ErrorAction SilentlyContinue | Should -BeNullOrEmpty Get-Command Initialize-NewLogEntryState -Module IT-ToolBox -ErrorAction SilentlyContinue | Should -BeNullOrEmpty diff --git a/Tests/Packaging.Tests.ps1 b/Tests/Packaging.Tests.ps1 index fa88f59..6cca2ec 100644 --- a/Tests/Packaging.Tests.ps1 +++ b/Tests/Packaging.Tests.ps1 @@ -4,13 +4,13 @@ BeforeAll { $package = & $buildScript -OutputDirectory (Join-Path $TestDrive 'output [literal]') $extractRoot = Join-Path $TestDrive 'extracted' [IO.Compression.ZipFile]::ExtractToDirectory($package.ArchivePath, $extractRoot) - $moduleRoot = Join-Path $extractRoot 'IT-ToolBox/3.1.0' + $moduleRoot = Join-Path $extractRoot 'IT-ToolBox/3.2.0' } Describe 'Installable module packaging' { It 'uses the manifest release version and conventional module layout' { - $package.Version | Should -Be '3.1.0' - [IO.Path]::GetFileName($package.ArchivePath) | Should -Be 'IT-ToolBox-3.1.0.zip' + $package.Version | Should -Be '3.2.0' + [IO.Path]::GetFileName($package.ArchivePath) | Should -Be 'IT-ToolBox-3.2.0.zip' Test-ModuleManifest (Join-Path $moduleRoot 'IT-ToolBox.psd1') -ErrorAction Stop | Should -Not -BeNullOrEmpty foreach ($name in @('LICENSE', 'CONTRIBUTING.md', 'SECURITY.md')) { Test-Path -LiteralPath (Join-Path $moduleRoot $name) | Should -BeTrue @@ -45,7 +45,7 @@ Describe 'Installable module packaging' { $code = @" `$ErrorActionPreference = 'Stop' `$env:PSModulePath = '$escapedRoot' + [IO.Path]::PathSeparator + `$env:PSModulePath -`$module = Import-Module IT-ToolBox -RequiredVersion 3.1.0 -Force -PassThru +`$module = Import-Module IT-ToolBox -RequiredVersion 3.2.0 -Force -PassThru New-LogEntry -LogMessage 'Packaged logger' -BufferOnly [pscustomobject]@{ ExportCount = `$module.ExportedFunctions.Count @@ -59,7 +59,7 @@ New-LogEntry -LogMessage 'Packaged logger' -BufferOnly $LASTEXITCODE | Should -Be 0 $result = $output | ConvertFrom-Json $result.ModuleBase | Should -Be $moduleRoot - $result.ExportCount | Should -Be 30 + $result.ExportCount | Should -Be 31 $result.Buffer.Count | Should -Be 1 $result.Buffer[0] | Should -Match '\[INFO\]: Packaged logger$' } diff --git a/Tests/RegistryKey.Tests.ps1 b/Tests/RegistryKey.Tests.ps1 new file mode 100644 index 0000000..0ff3d54 --- /dev/null +++ b/Tests/RegistryKey.Tests.ps1 @@ -0,0 +1,74 @@ +BeforeAll { + Import-Module (Join-Path $PSScriptRoot '../IT-ToolBox.psd1') -Force -ErrorAction Stop +} +Describe 'Registry key and value contracts with offline handles' { + BeforeEach { + $script:key = [pscustomobject]@{ Disposed = $false } + $script:key | Add-Member ScriptMethod Dispose { $this.Disposed = $true } + $script:key | Add-Member ScriptMethod GetValueNames { @('', 'Zero', 'Empty', 'A*B') } + Mock Open-ITToolBoxRegistryKey -ModuleName IT-ToolBox { $script:key } + } + It 'returns true for an existing key and disposes its handle' { + Test-IsRegistryKey -KeyPath 'HKCU:\Example' | Should -BeTrue + $script:key.Disposed | Should -BeTrue + } + It 'returns true for an empty key while value checks return false' { + $script:key | Add-Member ScriptMethod GetValueNames { @() } -Force + Test-IsRegistryKey 'HKCU:\Empty' | Should -BeTrue + Test-RegistryValue 'HKCU:\Empty' 'Missing' | Should -BeFalse + } + It 'returns false for a missing key in both commands' { + Mock Open-ITToolBoxRegistryKey -ModuleName IT-ToolBox { $null } + Test-IsRegistryKey 'HKCU:\Missing' | Should -BeFalse + Test-RegistryValue 'HKCU:\Missing' 'Name' | Should -BeFalse + } + It 'passes metacharacters literally to the shared opener' { + Test-IsRegistryKey 'HKCU:\Literal[1]*' | Should -BeTrue + Should -Invoke Open-ITToolBoxRegistryKey -ModuleName IT-ToolBox -Times 1 -Exactly -ParameterFilter { $Path -ceq 'HKCU:\Literal[1]*' } + } + It 'propagates access failures from both commands' { + Mock Open-ITToolBoxRegistryKey -ModuleName IT-ToolBox { throw [UnauthorizedAccessException]::new('Access denied') } + { Test-IsRegistryKey 'HKCU:\Denied' } | Should -Throw '*Access denied*' + { Test-RegistryValue 'HKCU:\Denied' 'Name' } | Should -Throw '*Access denied*' + } + It 'preserves default, case-insensitive and literal value-name checks' { + Test-RegistryValue 'HKCU:\Example' '' | Should -BeTrue + Test-RegistryValue 'HKCU:\Example' 'zero' | Should -BeTrue + Test-RegistryValue 'HKCU:\Example' 'A*B' | Should -BeTrue + Test-RegistryValue 'HKCU:\Example' 'A*' | Should -BeFalse + $script:key.Disposed | Should -BeTrue + } + It 'disposes value handles even if reading fails' { + $script:key | Add-Member ScriptMethod GetValueNames { throw 'Read failed' } -Force + { Test-RegistryValue 'HKCU:\Example' 'Name' } | Should -Throw '*Read failed*' + $script:key.Disposed | Should -BeTrue + } + It 'rejects null and empty key arguments during binding' { + { Test-IsRegistryKey -KeyPath $null } | Should -Throw + { Test-IsRegistryKey -KeyPath '' } | Should -Throw + Should -Invoke Open-ITToolBoxRegistryKey -ModuleName IT-ToolBox -Times 0 -Exactly + } +} +Describe 'Registry key platform boundary' { + It 'rejects invocation on non-Windows without affecting module import' -Skip:$IsWindows { + { Test-IsRegistryKey 'HKCU:\Software' } | Should -Throw '*Windows Registry provider*' + } + It 'checks actual literal empty HKCU keys on Windows' -Skip:(-not $IsWindows) { + $parent = 'HKCU:\Software\ITToolBoxKeyTests-' + [guid]::NewGuid().ToString('N') + try { + $null = New-Item -Path $parent -ErrorAction Stop + $key = Get-Item -LiteralPath $parent -ErrorAction Stop + try { $child = $key.CreateSubKey('Literal[1]') } + finally { $key.Dispose() } + $child.Dispose() + Test-IsRegistryKey ($parent + '\Literal[1]') | Should -BeTrue + Test-IsRegistryKey ($parent + '\Literal*') | Should -BeFalse + Test-IsRegistryKey ($parent + '\Missing') | Should -BeFalse + Test-RegistryValue ($parent + '\Literal[1]') 'Missing' | Should -BeFalse + { Test-IsRegistryKey $TestDrive } | Should -Throw '*Registry provider*' + } + finally { + if (Test-Path -LiteralPath $parent) { Remove-Item -LiteralPath $parent -Recurse -Force } + } + } +} diff --git a/docs/Packaging.md b/docs/Packaging.md index c71cede..c5df513 100644 --- a/docs/Packaging.md +++ b/docs/Packaging.md @@ -1,7 +1,7 @@ # Build and install the module -Build from the reviewed 3.1.0 release commit or tag using PowerShell 7.4 or later. -The manifest version is 3.1.0, with no prerelease suffix. Preserve that source +Build from the reviewed 3.2.0 release commit or tag using PowerShell 7.4 or later. +The manifest version is 3.2.0, with no prerelease suffix. Preserve that source revision alongside the archive when distributing it. Run from the repository root: @@ -11,12 +11,12 @@ Invoke-Pester ./Tests ./scripts/Build-Module.ps1 ``` -The script creates artifacts/IT-ToolBox-3.1.0.zip and returns its path, +The script creates artifacts/IT-ToolBox-3.2.0.zip and returns its path, version and file count. It takes the version from the manifest without modifying it. Use -OutputDirectory to choose another local destination. An existing archive causes a terminating error; use a different destination for another build. -The ZIP contains IT-ToolBox/3.1.0/ with the manifest, loader, Public and Private +The ZIP contains IT-ToolBox/3.2.0/ with the manifest, loader, Public and Private implementation files, license, README, changelog, contributing guide, security policy and Markdown documents in docs/. Tests, build scripts, Legacy, Staging, Git metadata and external binaries are @@ -26,8 +26,8 @@ in a separate session; the old decoder is not distributed in this archive. Extract into a new directory, then verify or import the extracted manifest: ```powershell -Expand-Archive -LiteralPath './artifacts/IT-ToolBox-3.1.0.zip' -DestinationPath './module-install' -Import-Module './module-install/IT-ToolBox/3.1.0/IT-ToolBox.psd1' -Force +Expand-Archive -LiteralPath './artifacts/IT-ToolBox-3.2.0.zip' -DestinationPath './module-install' +Import-Module './module-install/IT-ToolBox/3.2.0/IT-ToolBox.psd1' -Force Get-Command -Module IT-ToolBox ```