From 8a6c4f83613d3612b345fbebcea9ce5940d6af00 Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Thu, 24 Sep 2026 17:44:45 -0500 Subject: [PATCH 1/2] feat: add Invoke-PiHoleFlushLogs Implements POST /action/flush/logs, one of the two remaining "Actions" gaps from the API coverage audit. Flushes the DNS log file and purges the most recent 24 hours of query history from both the database and FTL's internal memory. The other gap, POST /action/flush/arp, is deprecated by Pi-hole itself in favor of /action/flush/network ("Deprecated! Use '/action/flush/network' instead."), which the module already implements via Invoke-PiHoleFlushNetwork - so it's intentionally not being added as a separate function. Also fixed a copy-paste bug found along the way: Invoke-PiHoleFlushNetwork's .DESCRIPTION described flushing a log file, which is what this new function actually does, not what flushing the network table does. Verified against a real Pi-hole v6 server: formatted output, RawOutput, and bad-password error handling. Added a dedicated integration test file. README regenerated. Co-Authored-By: Claude Sonnet 5 --- PiHoleShell/PiHoleShell.psm1 | 2 +- .../Public/Actions/Invoke-PiHoleFlushLogs.ps1 | 71 +++++++++++++++++++ .../Actions/Invoke-PiHoleFlushNetwork.ps1 | 2 +- README.md | 1 + ...voke-PiHoleFlushLogs.Integration.Tests.ps1 | 46 ++++++++++++ 5 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 PiHoleShell/Public/Actions/Invoke-PiHoleFlushLogs.ps1 create mode 100644 tests/Invoke-PiHoleFlushLogs.Integration.Tests.ps1 diff --git a/PiHoleShell/PiHoleShell.psm1 b/PiHoleShell/PiHoleShell.psm1 index 92b77e2..001e1ec 100644 --- a/PiHoleShell/PiHoleShell.psm1 +++ b/PiHoleShell/PiHoleShell.psm1 @@ -16,7 +16,7 @@ foreach ($File in $PrivateFunctions) { Export-ModuleMember -Function @( #Actions - 'Update-PiHoleActionsGravity', 'Invoke-PiHoleFlushNetwork', 'Restart-PiHoleDnsService' ` + 'Update-PiHoleActionsGravity', 'Invoke-PiHoleFlushNetwork', 'Invoke-PiHoleFlushLogs', 'Restart-PiHoleDnsService' ` #Authentication 'Remove-PiHoleCurrentAuthSession' , 'Get-PiHoleCurrentAuthSession', 'Remove-PiHoleAuthSession', ` #GroupManagement diff --git a/PiHoleShell/Public/Actions/Invoke-PiHoleFlushLogs.ps1 b/PiHoleShell/Public/Actions/Invoke-PiHoleFlushLogs.ps1 new file mode 100644 index 0000000..ec03eb8 --- /dev/null +++ b/PiHoleShell/Public/Actions/Invoke-PiHoleFlushLogs.ps1 @@ -0,0 +1,71 @@ +function Invoke-PiHoleFlushLogs { + <# +.SYNOPSIS +Flushes the DNS logs + +.DESCRIPTION +Flushes the Pi-hole DNS logs. This empties the DNS log file and purges the most recent 24 +hours of query history from both the long-term database and FTL's internal memory. + +.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 +Invoke-PiHoleFlushLogs -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" + #> + [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#post-/action/flush/logs')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Flushes PiHole logs')] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification = "Logs matches the Pi-hole API's own endpoint name, /action/flush/logs")] + param ( + [Parameter(Mandatory = $true)] + [System.URI]$PiHoleServer, + [Parameter(Mandatory = $true)] + [string]$Password, + [bool]$IgnoreSsl = $false, + [bool]$RawOutput = $false + ) + + try { + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = "$($PiHoleServer.OriginalString)/api/action/flush/logs" + Method = "Post" + ContentType = "application/json" + SkipCertificateCheck = $IgnoreSsl + } + + $Response = Invoke-RestMethod @Params + + if ($RawOutput) { + Write-Output $Response + } + else { + $Object = [PSCustomObject]@{ + Status = "Flushed" + } + Write-Output $Object + } + } + + catch { + Write-Error -Message $_.Exception.Message + } + + finally { + if ($Sid) { + Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl + } + } +} diff --git a/PiHoleShell/Public/Actions/Invoke-PiHoleFlushNetwork.ps1 b/PiHoleShell/Public/Actions/Invoke-PiHoleFlushNetwork.ps1 index 2c47c34..d361243 100644 --- a/PiHoleShell/Public/Actions/Invoke-PiHoleFlushNetwork.ps1 +++ b/PiHoleShell/Public/Actions/Invoke-PiHoleFlushNetwork.ps1 @@ -4,7 +4,7 @@ function Invoke-PiHoleFlushNetwork { Flushes the network table. This includes removing both all known devices and their associated addresses. .DESCRIPTION -Flushes the Pi-hole log file (/var/log/pihole/pihole.log). +Flushes Pi-hole's network table, removing all known devices and their associated addresses. .PARAMETER PiHoleServer The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" diff --git a/README.md b/README.md index 7180dec..2a7c11a 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ Functions marked 🚧 are still under active development — signatures and outp | Function | Description | |---|---| +| `Invoke-PiHoleFlushLogs` | Flushes the DNS logs | | `Invoke-PiHoleFlushNetwork` | Flushes the network table. This includes removing both all known devices and their associated addresses. | | `Restart-PiHoleDnsService` | Restarts the pihole-FTL service | | `Update-PiHoleActionsGravity` | Update Pi-hole's adlists by running pihole -g | diff --git a/tests/Invoke-PiHoleFlushLogs.Integration.Tests.ps1 b/tests/Invoke-PiHoleFlushLogs.Integration.Tests.ps1 new file mode 100644 index 0000000..9a31e06 --- /dev/null +++ b/tests/Invoke-PiHoleFlushLogs.Integration.Tests.ps1 @@ -0,0 +1,46 @@ +# Requires -Module Pester +# +# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1 +# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically +# if that file is missing. + +# Config availability must be known at discovery time so the -Skip parameter on each It block +# (evaluated during discovery, before BeforeAll runs) sees the correct value. +$script:ConfigAvailable = Test-Path (Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1') + +Describe 'Invoke-PiHoleFlushLogs (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + # Recomputed here (not read from the discovery-time $script:ConfigAvailable above) because + # Pester runs discovery and run in separate scopes, so BeforeAll cannot see that value. + $configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + } + } + + It 'flushes the DNS logs and returns a formatted status' -Skip:(-not $script:ConfigAvailable) { + $result = Invoke-PiHoleFlushLogs -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Flushed' + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = Invoke-PiHoleFlushLogs -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result.status | Should -Be 'success' + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Invoke-PiHoleFlushLogs -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} From 705fdf20e067ffead7b77740fbb20ba6a8874c83 Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Thu, 24 Sep 2026 22:28:21 -0500 Subject: [PATCH 2/2] chore: move Invoke-PiHoleFlushLogs test into tests/Actions/ Merges the latest develop (which reorganized tests/ to mirror PiHoleShell/Public//) into this branch, then moves this PR's own new test file - added before that reorg landed - into tests/Actions/ to match, fixing its $PSScriptRoot-relative IntegrationConfig.local.ps1 lookup accordingly. Verified against the real Pi-hole server post-move. Co-Authored-By: Claude Sonnet 5 --- .../Invoke-PiHoleFlushLogs.Integration.Tests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename tests/{ => Actions}/Invoke-PiHoleFlushLogs.Integration.Tests.ps1 (90%) diff --git a/tests/Invoke-PiHoleFlushLogs.Integration.Tests.ps1 b/tests/Actions/Invoke-PiHoleFlushLogs.Integration.Tests.ps1 similarity index 90% rename from tests/Invoke-PiHoleFlushLogs.Integration.Tests.ps1 rename to tests/Actions/Invoke-PiHoleFlushLogs.Integration.Tests.ps1 index 9a31e06..0771c2d 100644 --- a/tests/Invoke-PiHoleFlushLogs.Integration.Tests.ps1 +++ b/tests/Actions/Invoke-PiHoleFlushLogs.Integration.Tests.ps1 @@ -6,7 +6,7 @@ # Config availability must be known at discovery time so the -Skip parameter on each It block # (evaluated during discovery, before BeforeAll runs) sees the correct value. -$script:ConfigAvailable = Test-Path (Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1') +$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1') Describe 'Invoke-PiHoleFlushLogs (Integration)' -Tag 'Integration' { BeforeAll { @@ -14,7 +14,7 @@ Describe 'Invoke-PiHoleFlushLogs (Integration)' -Tag 'Integration' { # Recomputed here (not read from the discovery-time $script:ConfigAvailable above) because # Pester runs discovery and run in separate scopes, so BeforeAll cannot see that value. - $configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1' + $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1' if (Test-Path $configPath) { . $configPath $script:PiHoleServer = $PiHoleServer