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
2 changes: 1 addition & 1 deletion PiHoleShell/PiHoleShell.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Export-ModuleMember -Function @(
'Get-PiHoleStatsRecentBlocked', 'Get-PiHoleStatsQueryType', 'Get-PiHoleStatsTopDomain', 'Get-PiHoleStatsSummary', 'Get-PiHoleStatsTopClient', 'Get-PiHoleStatsQuerySuggestions', `
'Get-PiHoleStatsUpstream', 'Get-PiHoleStatsDatabaseUpstream', 'Get-PiHoleStatsDatabaseSummary', 'Get-PiHoleStatsDatabaseTopDomain', 'Get-PiHoleStatsDatabaseTopClient', 'Get-PiHoleStatsDatabaseQueryType' `
#ListManagement
'Get-PiHoleList', 'Search-PiHoleListDomain', 'Add-PiHoleList', 'Remove-PiHoleList', `
'Get-PiHoleList', 'Search-PiHoleListDomain', 'Add-PiHoleList', 'Remove-PiHoleList', 'Update-PiHoleList', `
#FTLInformation
'Get-PiHoleInfoMessage', 'Get-PiHoleInfoHost', 'Get-PiHoleInfoClient', 'Get-PiHoleInfoLogin', 'Get-PiHoleInfoSystem', 'Get-PiHoleInfoFtl', `
'Get-PiHoleInfoSensors', 'Get-PiHoleInfoDatabase', 'Get-PiHoleInfoVersion', 'Get-PiHoleInfoMetrics', 'Get-PiHoleInfoMessageCount', 'Remove-PiHoleInfoMessage', `
Expand Down
152 changes: 152 additions & 0 deletions PiHoleShell/Public/ListManagement/Update-PiHoleList.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
function Update-PiHoleList {
<#
.SYNOPSIS
Update a list

.DESCRIPTION
Updates an existing list's Comment, Group(s), and/or Enabled state. The underlying Pi-hole API
replaces the entire list on update, so any property you don't pass here is preserved by first
reading the list's current value and resending it - nothing is silently cleared just because
you only meant to change one property.

.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 Address
The URL of the list to update

.PARAMETER Type
Whether this is an Allow list or a Block list

.PARAMETER Comment
The new comment for the list. Leave unset to keep the list's current comment

.PARAMETER Group
The group(s) this list should apply to. Leave unset to keep the list's current group(s)

.PARAMETER Enabled
Whether the list should be enabled. Leave unset to keep the list's current state

.PARAMETER IgnoreSsl
Set to $true to skip SSL certificate validation

.PARAMETER RawOutput
This will dump the response instead of the formatted object

.EXAMPLE
Update-PiHoleList -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Address "https://hosts-file.net/ad_servers.txt" -Type Block -Enabled $false
#>
[CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#put-/lists/-list-')]
[Diagnostics.CodeAnalysis.SuppressMessage("PSUseShouldProcessForStateChangingFunctions", "", Justification = "Ignoring for now")]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")]
param (
[Parameter(Mandatory = $true)]
[System.URI]$PiHoleServer,
[Parameter(Mandatory = $true)]
[string]$Password,
[Parameter(Mandatory = $true)]
[System.Uri]$Address,
[Parameter(Mandatory = $true)]
[ValidateSet("Allow", "Block")]
[string]$Type,
[string]$Comment,
[string[]]$Group,
[Nullable[bool]]$Enabled,
[bool]$IgnoreSsl = $false,
[bool]$RawOutput = $false
)

try {
if (-not $PSBoundParameters.ContainsKey('Comment') -and -not $PSBoundParameters.ContainsKey('Group') -and -not $PSBoundParameters.ContainsKey('Enabled')) {
throw "To update $Address, you must specify the Comment, Group, and/or Enabled parameter"
}

$ExistingList = Get-PiHoleList -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl -List $Address | Where-Object { $_.Type -eq $Type }

if (-not $ExistingList) {
throw "Cannot find $Address of type $Type on $PiHoleServer! Please use Add-PiHoleList to create it"
}

$AllGroups = Get-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl

$GroupNamesToResolve = if ($PSBoundParameters.ContainsKey('Group')) { $Group } else { $ExistingList.Groups }

$AllGroupsNames = @()
$AllGroupsIds = @()
foreach ($GroupItem in $GroupNamesToResolve) {
$FoundGroup = $AllGroups | Where-Object { $_.Name -eq $GroupItem }
if ($FoundGroup) {
$AllGroupsNames += $FoundGroup.Name
$AllGroupsIds += $FoundGroup.Id
}
else {
throw "Cannot find $GroupItem on $PiHoleServer! Please use Get-PiHoleGroup to list all groups"
}
}

$Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl

# The API replaces the whole list on update, so any property not explicitly passed here
# is resent using the list's current value to avoid silently clearing it.
$Body = @{
comment = if ($PSBoundParameters.ContainsKey('Comment')) { $Comment } else { $ExistingList.Comment }
groups = [Object[]]($AllGroupsIds)
enabled = if ($PSBoundParameters.ContainsKey('Enabled')) { $Enabled } else { $ExistingList.Enabled }
}

$Params = @{
Headers = @{sid = $($Sid) }
Uri = "$($PiHoleServer.OriginalString)/api/lists/$Address`?type=$($Type.ToLower())"
Method = "Put"
SkipCertificateCheck = $IgnoreSsl
Body = $Body | ConvertTo-Json -Depth 10
ContentType = "application/json"
}

$Response = Invoke-RestMethod @Params

if ($RawOutput) {
Write-Output $Response
}
else {
$ObjectFinal = foreach ($Item in $Response.lists) {
if ($Item.date_updated -eq 0) {
$DateUpdated = $null
}
else {
$DateUpdated = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.date_modified).LocalTime
}

[PSCustomObject]@{
Address = $Item.address
Comment = $Item.comment
Groups = $AllGroupsNames
Enabled = $Item.enabled
Id = $Item.id
DateAdded = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.date_added).LocalTime
DateModified = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.date_modified).LocalTime
Type = $Item.type.SubString(0, 1).ToUpper() + $Item.type.SubString(1).ToLower()
DateUpdated = $DateUpdated
Number = $Item.number
InvalidDomains = $Item.invalid_domains
AbpEntries = $Item.abp_entries
Status = $Item.status
}
}
Write-Output $ObjectFinal
}
}

catch {
Write-Error -Message $_.Exception.Message
}

finally {
if ($Sid) {
Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl
}
}
}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ Functions marked 🚧 are still under active development — signatures and outp
| `Get-PiHoleList` 🚧 | Get lists |
| `Remove-PiHoleList` | Remove a list |
| `Search-PiHoleListDomain` | _No description yet_ |
| `Update-PiHoleList` | Update a list |

### Metrics

Expand Down
94 changes: 94 additions & 0 deletions tests/ListManagement/Update-PiHoleList.Integration.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# 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.

$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1')

Describe 'Update-PiHoleList (Integration)' -Tag 'Integration' {
BeforeAll {
Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force

$script:TestListAddress = 'https://blocklistproject.github.io/Lists/alt-version/ransomware-nl.txt'

$configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1'
if (Test-Path $configPath) {
. $configPath
$script:PiHoleServer = $PiHoleServer
$script:PiHoleToken = $PiHoleToken
$script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl

# Defensive cleanup in case a previous failed run left the test list behind
Remove-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
}
}

AfterAll {
if ($script:PiHoleServer) {
Remove-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
}
}

It 'updates only the comment, preserving Enabled and Group' -Skip:(-not $script:ConfigAvailable) {
Add-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block -Comment 'original comment' -Enabled $true | Out-Null

$result = Update-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block -Comment 'updated comment'
$result | Format-List | Out-String | Write-Host

$result | Should -Not -BeNullOrEmpty
$result.Comment | Should -Be 'updated comment'
$result.Enabled | Should -BeTrue
$result.Groups | Should -Contain 'Default'

Remove-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block -Confirm:$false | Out-Null
}

It 'updates only Enabled, preserving the current comment' -Skip:(-not $script:ConfigAvailable) {
Add-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block -Comment 'keep this comment' -Enabled $true | Out-Null

$result = Update-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block -Enabled $false
$result | Format-List | Out-String | Write-Host

$result | Should -Not -BeNullOrEmpty
$result.Comment | Should -Be 'keep this comment'
$result.Enabled | Should -BeFalse

Remove-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block -Confirm:$false | Out-Null
}

It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) {
Add-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block | Out-Null

$result = Update-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block -Comment 'raw output test' -RawOutput $true
$result | Format-List | Out-String | Write-Host

$result.lists[0].comment | Should -Be 'raw output test'

Remove-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block -Confirm:$false | Out-Null
}

It 'errors when neither Comment, Group, nor Enabled is specified' -Skip:(-not $script:ConfigAvailable) {
Add-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block | Out-Null

$result = Update-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block -ErrorVariable errOut -ErrorAction SilentlyContinue

$errOut | Should -Not -BeNullOrEmpty

Remove-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block -Confirm:$false | Out-Null
}

It 'errors when the list does not exist' -Skip:(-not $script:ConfigAvailable) {
$result = Update-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address 'https://example.com/does-not-exist.txt' -Type Block -Comment 'irrelevant' -ErrorVariable errOut -ErrorAction SilentlyContinue

$errOut | Should -Not -BeNullOrEmpty
}

It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) {
Add-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block | Out-Null

$result = Update-PiHoleList -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block -Comment 'irrelevant' -ErrorVariable errOut -ErrorAction SilentlyContinue

$errOut | Should -Not -BeNullOrEmpty
}
}
Loading