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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
notes.txt
notes.txt
tests/IntegrationConfig.local.ps1
TestResults/
8 changes: 5 additions & 3 deletions PiHoleShell/Public/Actions/Invoke-PiHoleFlushNetwork.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ Invoke-PiHoleFlushNetwork -PiHoleServer "http://pihole.domain.com:8080" -Passwor
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Flushes PiHole logs')]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")]
param (
$PiHoleServer,
$Password,
[Parameter(Mandatory = $true)]
[System.URI]$PiHoleServer,
[Parameter(Mandatory = $true)]
[string]$Password,
[bool]$IgnoreSsl = $false,
[bool]$RawOutput = $false
)
Expand All @@ -36,7 +38,7 @@ Invoke-PiHoleFlushNetwork -PiHoleServer "http://pihole.domain.com:8080" -Passwor

$Params = @{
Headers = @{sid = $($Sid) }
Uri = "$PiHoleServer/api/action/flush/logs"
Uri = "$($PiHoleServer.OriginalString)/api/action/flush/network"
Method = "Post"
ContentType = "application/json"
SkipCertificateCheck = $IgnoreSsl
Expand Down
8 changes: 5 additions & 3 deletions PiHoleShell/Public/Actions/Restart-PiHoleDnsService.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ Invoke-PiHoleRestartDns -PiHoleServer "http://pihole.domain.com:8080" -Password
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Restarts PiHole DNS')]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")]
param (
$PiHoleServer,
$Password,
[Parameter(Mandatory = $true)]
[System.URI]$PiHoleServer,
[Parameter(Mandatory = $true)]
[string]$Password,
[bool]$IgnoreSsl = $false,
[bool]$RawOutput = $false
)
Expand All @@ -36,7 +38,7 @@ Invoke-PiHoleRestartDns -PiHoleServer "http://pihole.domain.com:8080" -Password

$Params = @{
Headers = @{sid = $($Sid) }
Uri = "$PiHoleServer/api/action/restartdns"
Uri = "$($PiHoleServer.OriginalString)/api/action/restartdns"
Method = "Post"
ContentType = "application/json"
SkipCertificateCheck = $IgnoreSsl
Expand Down
174 changes: 155 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,170 @@
# PiHoleShell
A PowerShell module for PiHole v6 API.

[![PowerShell Gallery Version](https://img.shields.io/powershellgallery/v/PiHoleShell?label=PowerShell%20Gallery)](https://www.powershellgallery.com/packages/PiHoleShell)
[![PowerShell Gallery Downloads](https://img.shields.io/powershellgallery/dt/PiHoleShell)](https://www.powershellgallery.com/packages/PiHoleShell)
[![CI](https://github.com/mikemadeja/PiHoleShell/actions/workflows/PSScriptAnalyzer.yml/badge.svg)](https://github.com/mikemadeja/PiHoleShell/actions/workflows/PSScriptAnalyzer.yml)
[![License: Apache 2.0](https://img.shields.io/github/license/mikemadeja/PiHoleShell)](LICENSE)
![PowerShell 7+](https://img.shields.io/badge/PowerShell-7%2B%20(Core)-blue)

A PowerShell module for automating and scripting against the **Pi-hole v6 REST API** — DNS blocking control, allow/deny lists, groups, stats, and server actions, all from PowerShell.

> This module targets Pi-hole's v6 API only. It will not work against Pi-hole v5 or earlier.

## Table of Contents

- [Features](#features)
- [Requirements](#requirements)
- [Installation](#installation)
- [Getting an API Password](#getting-an-api-password)
- [Quick Start](#quick-start)
- [Command Reference](#command-reference)
- [Testing](#testing)
- [Contributing](#contributing)
- [License](#license)

## Features

- Enable/disable DNS blocking, optionally for a set duration
- Manage allow/deny lists and groups
- Trigger server actions: flush network table, restart DNS, update gravity
- Pull stats, summaries, and diagnostic info
- Every function authenticates and closes its own session automatically — no manual login/logout calls needed

## Requirements

- **PowerShell 7.0+ (Core edition)** — the module refuses to load on Windows PowerShell 5.1 or other editions
- A reachable Pi-hole v6 server and an API app password

## Installation

It is recommended to install this from https://www.powershellgallery.com/packages/PiHoleShell
Install from the [PowerShell Gallery](https://www.powershellgallery.com/packages/PiHoleShell):

```powershell
Install-Module -Name PiHoleShell -Scope CurrentUser
Import-Module -Name PiHoleShell
```

## Contributions
## Getting an API Password

I am in the beginning stages of developing this. I am open to suggestions or contributors and am learning as I go. I hope this will bring some value to people :-).
1. Log into your Pi-hole web interface, then go to **Web Interface / API** settings and select **Configure app password**.

## How to use
<img src="docs/images/webinterfance_api.png" alt="Pi-hole Web Interface / API settings" width="450"/>

Generate an app password by logging into your PiHole server.
2. Copy the generated password, then click **Enable new app password**.

Click Web Interface / Api
<img src="docs/images/configure_app_password.png" alt="Configure app password dialog" width="450"/>

Click Configure app password
Keep this password secret — anyone with it has full API access to your Pi-hole.

<img src="docs\images\webinterfance_api.png" alt="drawing" width="450"/>
## Quick Start

Copy your password, then click Enable new app password.
```powershell
$PiHoleServer = "https://pihole.example.com:8489"
$Password = "<your-app-password>"

<img src="docs\images\configure_app_password.png" alt="drawing" width="450"/>
# Check whether blocking is currently enabled
Get-PiHoleDnsBlockingStatus -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl:$true

# Disable blocking for 5 minutes, then it re-enables automatically
Set-PiHoleDnsBlocking -PiHoleServer $PiHoleServer -Password $Password -Blocking False -TimeInSeconds 300 -IgnoreSsl:$true
```
PS Install-Module -Name PiHoleShell
PS Import-Module -Name PiHoleShell
PS Get-PiHoleDnsBlockingStatus -PiHoleServer http://PIHOLESERVER.DOMAIN.COM -Password "APPPASSWORD" -IgnoreSsl:$true

Blocking Timer
-------- -----
enabled 0
```

Every function accepts the same core parameters:

| Parameter | Description |
|---|---|
| `-PiHoleServer` | Base URL of your Pi-hole, e.g. `https://pihole.example.com:8489` |
| `-Password` | The app password from [Getting an API Password](#getting-an-api-password) |
| `-IgnoreSsl` | Skip TLS certificate validation (useful for self-signed certs) |
| `-RawOutput` | Return the unmodified API response instead of a formatted object |

## Command Reference

Functions marked 🚧 are still under active development — signatures and output shapes may change.

### Actions

| Function | Description |
|---|---|
| `Invoke-PiHoleFlushNetwork` | Flush the network table, removing known devices and their addresses |
| `Restart-PiHoleDnsService` | Restart the `pihole-FTL` service |
| `Update-PiHoleActionsGravity` 🚧 | Run `pihole -g` to rebuild the gravity/adlists database |

### DNS Control

| Function | Description |
|---|---|
| `Get-PiHoleDnsBlockingStatus` | Get current blocking status and any active timer |
| `Set-PiHoleDnsBlocking` | Enable or disable blocking, optionally for a set duration |

### Group Management

| Function | Description |
|---|---|
| `Get-PiHoleGroup` | List groups |
| `New-PiHoleGroup` | Create a group |
| `Update-PiHoleGroup` | Update an existing group |
| `Remove-PiHoleGroup` 🚧 | Delete a group |

### List Management

| Function | Description |
|---|---|
| `Get-PiHoleList` 🚧 | List allow/deny lists |
| `Add-PiHoleList` 🚧 | Add a domain to an allow/deny list |
| `Remove-PiHoleList` 🚧 | Remove lists |
| `Search-PiHoleListDomain` | Search all lists for a domain, with optional partial matching |

### Metrics

| Function | Description |
|---|---|
| `Get-PiHoleStatsSummary` | Overview of query, system, and FTL activity |
| `Get-PiHoleStatsRecentBlocked` | Most recently blocked domain |
| `Get-PiHoleStatsQueryType` | Query breakdown by DNS record type |
| `Get-PiHoleStatsTopDomain` | Top permitted/blocked domains |
| `Get-PiHoleStatsTopClient` | Top clients by query volume |

### Configuration & Diagnostics

| Function | Description |
|---|---|
| `Get-PiHoleConfig` 🚧 | Read the Pi-hole configuration |
| `Get-PiHolePadd` 🚧 | Data used to power the PADD dashboard |
| `Get-PiHoleInfoMessage` | Pi-hole diagnosis messages |
| `Get-PiHoleInfoHost` 🚧 | Host system information |

### Authentication

Session handling is automatic for every command above, but these are available for managing sessions directly:

| Function | Description |
|---|---|
| `Get-PiHoleCurrentAuthSession` | List active API sessions |
| `Remove-PiHoleAuthSession` | Revoke a session by ID |

## Testing

The module ships with two kinds of [Pester](https://pester.dev/) tests under `tests/`:

- **Unit tests** (`*.Tests.ps1`) mock the API and run anywhere:

```powershell
Invoke-Pester -Path .\tests -ExcludeTagFilter Integration
```

- **Integration tests** (`*.Integration.Tests.ps1`) run against a real Pi-hole server and are skipped automatically unless configured. To run them, copy `tests/IntegrationConfig.example.ps1` to `tests/IntegrationConfig.local.ps1` (gitignored) and fill in your server URL and app password, then run:

```powershell
Invoke-Pester -Path .\tests -TagFilter Integration
```

These make real changes on the target server (they flush the network table, restart DNS, and rebuild gravity) — point them at a test instance, not production, if you'd rather not disrupt it.

## Contributing

This project is still early and growing. Issues, suggestions, and pull requests are welcome — see the 🚧 items in the [Command Reference](#command-reference) above for functions that could use testing or polish.

## License

[Apache License 2.0](LICENSE)
69 changes: 69 additions & 0 deletions azuredevops-pihole-pester-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
trigger:
branches:
include:
- main
- develop

pr:
branches:
include:
- main
- develop

pool:
name: mmadeja-dt

variables:
testResultsFile: '$(System.DefaultWorkingDirectory)/TestResults/pester.xml'

steps:
- pwsh: |
$pester = Get-Module -ListAvailable -Name Pester | Where-Object { $_.Version -ge [version]'5.0.0' } | Select-Object -First 1
if (-not $pester) {
Write-Host "Installing Pester 5..."
Install-Module -Name Pester -MinimumVersion 5.0.0 -Force -SkipPublisherCheck -Scope CurrentUser
}
else {
Write-Host "Using Pester $($pester.Version)"
}
displayName: 'Ensure Pester 5 is available'

- pwsh: |
$configContent = @"
`$PiHoleServer = [uri]'$env:PIHOLE_TEST_SERVER'
`$PiHoleToken = '$env:PIHOLE_TEST_TOKEN'
`$PiHoleIgnoreSsl = `$true
"@
Set-Content -Path (Join-Path '$(System.DefaultWorkingDirectory)' 'tests/IntegrationConfig.local.ps1') -Value $configContent -Encoding utf8
displayName: 'Write integration test config'
env:
PIHOLE_TEST_SERVER: $(PiHoleTestServer)
PIHOLE_TEST_TOKEN: $(PiHoleTestToken)

- pwsh: |
Import-Module Pester -MinimumVersion 5.0.0 -Force

$config = New-PesterConfiguration
$config.Run.Path = './tests'
$config.Run.Exit = $true
$config.Output.Verbosity = 'Detailed'
$config.TestResult.Enabled = $true
$config.TestResult.OutputFormat = 'NUnitXml'
$config.TestResult.OutputPath = '$(testResultsFile)'

Invoke-Pester -Configuration $config
displayName: 'Run Pester tests'
workingDirectory: '$(System.DefaultWorkingDirectory)'

- pwsh: |
Remove-Item -Path (Join-Path '$(System.DefaultWorkingDirectory)' 'tests/IntegrationConfig.local.ps1') -Force -ErrorAction SilentlyContinue
displayName: 'Clean up integration test config'
condition: always()

- task: PublishTestResults@2
displayName: 'Publish test results'
condition: succeededOrFailed()
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '$(testResultsFile)'
failTaskOnFailedTests: false
9 changes: 9 additions & 0 deletions tests/IntegrationConfig.example.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Copy this file to 'IntegrationConfig.local.ps1' (same folder) and fill in your own
# server details. IntegrationConfig.local.ps1 is gitignored so your token is never committed.
#
# These values are consumed by the *.Integration.Tests.ps1 files, which make real calls
# against a live Pi-hole server.

$PiHoleServer = [uri]'https://pihole.example.com:8489'
$PiHoleToken = 'your-api-token-here'
$PiHoleIgnoreSsl = $true
43 changes: 43 additions & 0 deletions tests/Invoke-PiHoleFlushNetwork.Integration.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# 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-PiHoleFlushNetwork (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 network table and returns a formatted status' -Skip:(-not $script:ConfigAvailable) {
$result = Invoke-PiHoleFlushNetwork -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl

$result | Should -Not -BeNullOrEmpty
$result.Status | Should -Be 'Flushed'
}

It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) {
{ Invoke-PiHoleFlushNetwork -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true } |
Should -Not -Throw
}

It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) {
$result = Invoke-PiHoleFlushNetwork -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue

$errOut | Should -Not -BeNullOrEmpty
}
}
Loading
Loading