From e8c8da5d220528c1a38377238751cf17c7e279c8 Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Sun, 20 Sep 2026 08:58:19 -0500 Subject: [PATCH 1/5] fix: Get-PiHoleInfoHost now returns Model and DMI fields It previously only mapped uname.* fields (DomainName, Machine, NodeName, Release, SysName, Version), silently dropping the API response's model and dmi (bios/board/product/sys vendor info) sections entirely - the same kind of completeness gap found in Get-PiHoleConfig. Also fixed a latent bug where Write-Output was called inside the foreach loop over $Response.host (a single object, not a collection), and dropped the stray `break` in its catch block. Kept the output flat (matching Get-PiHoleDnsBlockingStatus's simple style) rather than nesting it like the Get-PiHoleConfig fix, adding Model, BiosVendor, BoardName/Vendor/Version, ProductName/Family/Version, and SysVendor as top-level properties alongside the existing uname fields. Verified against a real server; regenerated the README command reference since Get-PiHoleInfoHost's placeholder docstring update means it's no longer flagged as a work-in-progress function. Co-Authored-By: Claude Sonnet 5 --- .../FTLInformation/Get-PiHoleInfoHost.ps1 | 57 ++++++++++++------- README.md | 2 +- .../Get-PiHoleInfoHost.Integration.Tests.ps1 | 1 + 3 files changed, 39 insertions(+), 21 deletions(-) diff --git a/PiHoleShell/Public/FTLInformation/Get-PiHoleInfoHost.ps1 b/PiHoleShell/Public/FTLInformation/Get-PiHoleInfoHost.ps1 index 6ea7d84..c573354 100644 --- a/PiHoleShell/Public/FTLInformation/Get-PiHoleInfoHost.ps1 +++ b/PiHoleShell/Public/FTLInformation/Get-PiHoleInfoHost.ps1 @@ -1,11 +1,27 @@ function Get-PiHoleInfoHost { <# .SYNOPSIS -Get info about various host parameters -This API hook returns a collection of host infos. +Get information about the host system +.DESCRIPTION +Request host system information: kernel/OS details (uname), the hardware model, and DMI +(motherboard/BIOS) details where available. + +.PARAMETER PiHoleServer +The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" + +.PARAMETER Password +The API Password you generated from your PiHole server + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Get-PiHoleInfoHost -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" #> - #Work In Progress [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/info/host')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] param ( @@ -32,29 +48,30 @@ This API hook returns a collection of host infos. if ($RawOutput) { Write-Output $Response } - else { - $ObjectFinal = @() - foreach ($Item in $Response.host) { - $Object = $null - $Object = [PSCustomObject]@{ - DomainName = $Item.uname.domainname - Machine = $Item.uname.machine - NodeName = $Item.uname.nodename - Release = $Item.uname.release - SysName = $Item.uname.sysname - Version = $Item.uname.version - - } - $ObjectFinal += $Object - Write-Output $ObjectFinal + $Object = [PSCustomObject]@{ + DomainName = $Response.host.uname.domainname + Machine = $Response.host.uname.machine + NodeName = $Response.host.uname.nodename + Release = $Response.host.uname.release + SysName = $Response.host.uname.sysname + Version = $Response.host.uname.version + Model = $Response.host.model + BiosVendor = $Response.host.dmi.bios.vendor + BoardName = $Response.host.dmi.board.name + BoardVendor = $Response.host.dmi.board.vendor + BoardVersion = $Response.host.dmi.board.version + ProductName = $Response.host.dmi.product.name + ProductFamily = $Response.host.dmi.product.family + ProductVersion = $Response.host.dmi.product.version + SysVendor = $Response.host.dmi.sys.vendor } + Write-Output $Object } } catch { Write-Error -Message $_.Exception.Message - break } finally { @@ -62,4 +79,4 @@ This API hook returns a collection of host infos. Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl } } -} \ No newline at end of file +} diff --git a/README.md b/README.md index d14d073..31ddd4f 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ Functions marked 🚧 are still under active development — signatures and outp | Function | Description | |---|---| | `Get-PiHoleConfig` | Get current configuration of Pi-hole | -| `Get-PiHoleInfoHost` 🚧 | Get info about various host parameters This API hook returns a collection of host infos. | +| `Get-PiHoleInfoHost` | Get information about the host system | | `Get-PiHoleInfoMessage` | Get Pi-hole diagnosis messages Request Pi-hole diagnosis messages | | `Get-PiHolePadd` | Get summarized data for PADD | diff --git a/tests/Get-PiHoleInfoHost.Integration.Tests.ps1 b/tests/Get-PiHoleInfoHost.Integration.Tests.ps1 index 0aa7b03..3ca7e8a 100644 --- a/tests/Get-PiHoleInfoHost.Integration.Tests.ps1 +++ b/tests/Get-PiHoleInfoHost.Integration.Tests.ps1 @@ -25,6 +25,7 @@ Describe 'Get-PiHoleInfoHost (Integration)' -Tag 'Integration' { $result | Should -Not -BeNullOrEmpty $result.NodeName | Should -Not -BeNullOrEmpty + $result.Model | Should -Not -BeNullOrEmpty } It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { From 243ca71f5c3e2e84bc4d08f21013884f0e05bb24 Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Sun, 20 Sep 2026 09:52:03 -0500 Subject: [PATCH 2/5] fix: Get-PiHoleStatsSummary was dropping half the response The formatted output only ever captured Total, Blocked, PercentBlocked, Types, Status, and Replies. The API also returns UniqueDomains, Forwarded, Cached, and Frequency alongside those (all siblings under queries), plus two entire top-level sections - Clients{Active,Total} and Gravity{DomainsBeingBlocked,LastUpdate} - that were never mapped at all. Also dropped the stray `break` in the catch block and the $ObjectFinal += pattern in favor of a direct Write-Output, matching the module's standard shape, and cleaned up a docstring artifact where .PARAMETER Password had leftover RawOutput description text pasted into it. Verified against a real server - all previously-missing fields now present with real values. Co-Authored-By: Claude Sonnet 5 --- .../Public/Metrics/Get-PiHoleStatsSummary.ps1 | 22 ++++++++++++++----- ...t-PiHoleStatsSummary.Integration.Tests.ps1 | 2 ++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/PiHoleShell/Public/Metrics/Get-PiHoleStatsSummary.ps1 b/PiHoleShell/Public/Metrics/Get-PiHoleStatsSummary.ps1 index 18c1fbb..9712c83 100644 --- a/PiHoleShell/Public/Metrics/Get-PiHoleStatsSummary.ps1 +++ b/PiHoleShell/Public/Metrics/Get-PiHoleStatsSummary.ps1 @@ -10,8 +10,8 @@ The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "h .PARAMETER Password The API Password you generated from your PiHole server - -This will dump the response instead of the formatted object +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation .PARAMETER RawOutput This will dump the response instead of the formatted object @@ -50,6 +50,10 @@ Get-PiHoleStatsSummary -PiHoleServer "http://pihole.domain.com:8080" -Password " Total = $Response.queries.total Blocked = $Response.queries.blocked PercentBlocked = $Response.queries.percent_blocked + UniqueDomains = $Response.queries.unique_domains + Forwarded = $Response.queries.forwarded + Cached = $Response.queries.cached + Frequency = $Response.queries.frequency Types = [PSCustomObject]@{ A = $Response.queries.types.A AAAA = $Response.queries.types.AAAA @@ -105,15 +109,21 @@ Get-PiHoleStatsSummary -PiHoleServer "http://pihole.domain.com:8080" -Password " None = $Response.queries.replies.NONE Blob = $Response.queries.replies.BLOB } + Clients = [PSCustomObject]@{ + Active = $Response.clients.active + Total = $Response.clients.total + } + Gravity = [PSCustomObject]@{ + DomainsBeingBlocked = $Response.gravity.domains_being_blocked + LastUpdate = $Response.gravity.last_update + } } - $ObjectFinal += $Object - Write-Output $ObjectFinal + Write-Output $Object } } catch { Write-Error -Message $_.Exception.Message - break } finally { @@ -121,4 +131,4 @@ Get-PiHoleStatsSummary -PiHoleServer "http://pihole.domain.com:8080" -Password " Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl } } -} \ No newline at end of file +} diff --git a/tests/Get-PiHoleStatsSummary.Integration.Tests.ps1 b/tests/Get-PiHoleStatsSummary.Integration.Tests.ps1 index 00aa057..dbd0004 100644 --- a/tests/Get-PiHoleStatsSummary.Integration.Tests.ps1 +++ b/tests/Get-PiHoleStatsSummary.Integration.Tests.ps1 @@ -31,6 +31,8 @@ Describe 'Get-PiHoleStatsSummary (Integration)' -Tag 'Integration' { $result.Types | Should -Not -BeNullOrEmpty $result.Status | Should -Not -BeNullOrEmpty $result.Replies | Should -Not -BeNullOrEmpty + $result.Clients | Should -Not -BeNullOrEmpty + $result.Gravity | Should -Not -BeNullOrEmpty } It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { From 75912f92cfd05006c12baff0bcd25a1fd3271681 Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Sun, 20 Sep 2026 09:52:14 -0500 Subject: [PATCH 3/5] fix: Get-PiHolePadd was missing QueryFrequency and leaking raw field names Queries.QueryFrequency (queries.query_frequency in the raw response) was never mapped. Separately, the System and Version sub-objects were assigned directly from the raw response ($Response.system / $Response.version), so their fields stayed snake_case instead of PascalCase like every other property on this object - now run through ConvertTo-PiHolePascalCaseObject for consistency and to avoid missing any of their fields by hand. Also removed the dead $ObjectFinal/$Object = $null setup and the array-wrapping pattern (Write-Output $Object directly instead), and dropped the stray `break` in the catch block, matching the module's standard shape. Co-Authored-By: Claude Sonnet 5 --- PiHoleShell/Public/Padd/Get-PiHolePadd.ps1 | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/PiHoleShell/Public/Padd/Get-PiHolePadd.ps1 b/PiHoleShell/Public/Padd/Get-PiHolePadd.ps1 index 662266f..c2b1a82 100644 --- a/PiHoleShell/Public/Padd/Get-PiHolePadd.ps1 +++ b/PiHoleShell/Public/Padd/Get-PiHolePadd.ps1 @@ -51,8 +51,6 @@ Get-PiHolePadd -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app } else { - $ObjectFinal = @() - $Object = $null $IFaceV4RxBytes = [PSCustomObject]@{ Value = $Response.iface.v4.rx_bytes.value Unit = $Response.iface.v4.rx_bytes.unit @@ -83,6 +81,7 @@ Get-PiHolePadd -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app Total = $Response.queries.total Blocked = $Response.queries.blocked PercentBlocked = $Response.queries.percent_blocked + QueryFrequency = $Response.queries.query_frequency } $Sensors = [PSCustomObject]@{ CpuTemp = $Response.sensors.cpu_temp @@ -121,22 +120,18 @@ Get-PiHolePadd -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app Queries = $Queries RecentBlocked = $Response.recent_blocked Sensors = $Sensors - System = $Response.system + System = ConvertTo-PiHolePascalCaseObject -InputObject $Response.system TopBlocked = $Response.top_blocked TopClient = $Response.top_client TopDomain = $Response.top_domain - Version = $Response.version + Version = ConvertTo-PiHolePascalCaseObject -InputObject $Response.version } - if ($Object) { - $ObjectFinal += $Object - } - Write-Output $ObjectFinal + Write-Output $Object } } catch { Write-Error -Message $_.Exception.Message - break } finally { From a512a4ddd1402a0bae5812cb884a02bb8224a04c Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Sun, 20 Sep 2026 09:52:26 -0500 Subject: [PATCH 4/5] fix: Get-PiHoleCurrentAuthSession dropped fields, filtered out your own session, and leaked SSL errors Three separate bugs found auditing this function: 1. Missing XForwardedFor and Cli fields from the response - never mapped. 2. The final pipeline silently filtered results to `Where-Object { $_.CurrentSession -match "False" }`, excluding the caller's own active session from a function whose entire purpose is "list of all current sessions." Removed the filter entirely; also the -match "False" against a boolean only ever worked by relying on stringification, not a real comparison. 3. The finally block's cleanup call (Remove-PiHoleCurrentAuthSession) never passed -IgnoreSsl, so it always attempted strict certificate validation regardless of the caller's setting - this is the exact cause of the "Failed to close Pi-hole session: The SSL connection could not be established" warning seen earlier this session against a self-signed-cert server. Also moved the auth call and request params inside the try block (they were outside it, unlike every other function in the module), and removed a dead `$ObjectFinal = @()` reset after the output had already been written. Verified against a real server: the caller's own current session (previously filtered out) now appears in the results, no SSL warning during cleanup, and XForwardedFor/Cli are present on every session. Co-Authored-By: Claude Sonnet 5 --- .../Get-PiHoleCurrentAuthSession.ps1 | 60 +++++++++---------- 1 file changed, 27 insertions(+), 33 deletions(-) diff --git a/PiHoleShell/Public/Authentication/Get-PiHoleCurrentAuthSession.ps1 b/PiHoleShell/Public/Authentication/Get-PiHoleCurrentAuthSession.ps1 index fdce234..9d1c146 100644 --- a/PiHoleShell/Public/Authentication/Get-PiHoleCurrentAuthSession.ps1 +++ b/PiHoleShell/Public/Authentication/Get-PiHoleCurrentAuthSession.ps1 @@ -29,48 +29,42 @@ Get-PiHoleCurrentAuthSession -PiHoleServer "http://pihole.domain.com:8080" -Pass [bool]$RawOutput = $false ) - $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + try { + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl - $Params = @{ - Headers = @{sid = $($Sid) } - Uri = "$($PiHoleServer.OriginalString)/api/auth/sessions" - Method = "Get" - SkipCertificateCheck = $IgnoreSsl - ContentType = "application/json" - } + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = "$($PiHoleServer.OriginalString)/api/auth/sessions" + Method = "Get" + SkipCertificateCheck = $IgnoreSsl + ContentType = "application/json" + } - try { $Response = Invoke-RestMethod @Params if ($RawOutput) { Write-Output $Response } - else { - if ($Response.Sessions) { - $ObjectFinal = @() - foreach ($Item in $Response.Sessions) { - $Object = [PSCustomObject]@{ - Id = $Item.id - CurrentSession = $Item.current_session - Valid = $Item.valid - TlsLogin = $Item.tls.login - TlsMixed = $Item.tls.mixed - LoginAt = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.login_at).LocalTime - LastActive = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.last_active).LocalTime - ValidUntil = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.valid_until).LocalTime - RemoteAddress = $Item.remote_addr - UserAgent = $Item.user_agent - App = $Item.app - } - - $ObjectFinal += $Object - $Object = $null + $ObjectFinal = foreach ($Item in $Response.sessions) { + [PSCustomObject]@{ + Id = $Item.id + CurrentSession = $Item.current_session + Valid = $Item.valid + TlsLogin = $Item.tls.login + TlsMixed = $Item.tls.mixed + LoginAt = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.login_at).LocalTime + LastActive = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.last_active).LocalTime + ValidUntil = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.valid_until).LocalTime + RemoteAddress = $Item.remote_addr + UserAgent = $Item.user_agent + XForwardedFor = $Item.x_forwarded_for + App = $Item.app + Cli = $Item.cli } } - Write-Output $ObjectFinal | Where-Object { $_.CurrentSession -match "False" } + Write-Output $ObjectFinal } - $ObjectFinal = @() } catch { @@ -79,7 +73,7 @@ Get-PiHoleCurrentAuthSession -PiHoleServer "http://pihole.domain.com:8080" -Pass finally { if ($Sid) { - Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid + Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl } } -} \ No newline at end of file +} From 8a53f83280eb6856dff5f22d23fd8e678749ad40 Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Mon, 21 Sep 2026 16:32:14 -0500 Subject: [PATCH 5/5] ci: check README command reference on every PR into develop Previously this only ran on develop->main promotion PRs, so drift introduced by a feature branch wouldn't be caught until much later, right before a release. Since develop now requires PRs for all changes anyway, running the same -Check gate on every PR targeting develop catches it right where it was introduced, before it merges anywhere - matching how PSScriptAnalyzer already gates every PR. Kept the main-branch trigger too, as a final safety net on promotion. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/SyncReadmeCommandReference.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/SyncReadmeCommandReference.yml b/.github/workflows/SyncReadmeCommandReference.yml index 3b9c3f1..ddd02d4 100644 --- a/.github/workflows/SyncReadmeCommandReference.yml +++ b/.github/workflows/SyncReadmeCommandReference.yml @@ -3,14 +3,13 @@ name: Sync README Command Reference on: pull_request: types: [opened, synchronize, reopened] - branches: [ "main" ] + branches: [ "develop", "main" ] permissions: contents: read jobs: sync-readme: - if: github.event.pull_request.head.ref == 'develop' runs-on: ubuntu-latest steps: @@ -20,10 +19,11 @@ jobs: ref: ${{ github.head_ref }} fetch-depth: 0 - # develop requires PRs for all changes, so this can't auto-commit/push a fix directly to - # develop (that used to work before branch protection was added, and now fails every time - # there's real drift to fix). Instead this just fails the check with instructions, the same - # way Invoke-ScriptAnalyzer already gates PSScriptAnalyzer.yml. + # Runs on every PR into develop (and into main, as a final check on promotion), so drift + # gets caught before it lands anywhere - develop requires PRs for all changes, so this + # can't auto-commit/push a fix directly to it (that used to work before branch protection + # was added). Instead this fails the check with instructions, the same way + # Invoke-ScriptAnalyzer already gates PSScriptAnalyzer.yml. - name: Check README command reference is up to date shell: pwsh run: ./tools/Update-ReadmeCommandReference.ps1 -Check