Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
5 changes: 3 additions & 2 deletions IT-ToolBox.psd1
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -26,6 +26,7 @@
'Convert-LogonTimestamp'
'Get-OsUpTime'
'Remove-SpecialCharacters'
'Test-IsRegistryKey'
'Test-RegistryValue'
'Test-IsValidDn'
'Test-IsValidUpn'
Expand All @@ -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'
Expand Down
1 change: 1 addition & 0 deletions IT-ToolBox.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Export-ModuleMember -Function @(
'Convert-LogonTimestamp'
'Get-OsUpTime'
'Remove-SpecialCharacters'
'Test-IsRegistryKey'
'Test-RegistryValue'
'Test-IsValidDn'
'Test-IsValidUpn'
Expand Down
25 changes: 25 additions & 0 deletions Private/Open-ITToolBoxRegistryKey.ps1
Original file line number Diff line number Diff line change
@@ -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
}
}
18 changes: 18 additions & 0 deletions Public/Test-IsRegistryKey.ps1
Original file line number Diff line number Diff line change
@@ -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() }
}
23 changes: 2 additions & 21 deletions Public/Test-RegistryValue.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
26 changes: 17 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -38,25 +38,25 @@ 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
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.
Expand Down Expand Up @@ -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. |
<!-- command-inventory:end -->

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Tests/IntegrationBoundary.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
5 changes: 3 additions & 2 deletions Tests/Module.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
10 changes: 5 additions & 5 deletions Tests/Packaging.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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$'
}
Expand Down
74 changes: 74 additions & 0 deletions Tests/RegistryKey.Tests.ps1
Original file line number Diff line number Diff line change
@@ -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 }
}
}
}
12 changes: 6 additions & 6 deletions docs/Packaging.md
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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
Expand All @@ -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
```

Expand Down
Loading