← All scripts

Email security & monitoring

Dark Web Breach Check (HIBP)

Checks every user in a Microsoft 365 tenant against Have I Been Pwned and produces a searchable HTML plus CSV breach report. Needs a paid HIBP API key, and uses the legacy AzureAD module (being retired).

Adapted from: Adapted from CyberDrain (Kelvin Tegelaar) ↗

Download .ps1117 lines · PowerShell

Replace generic placeholder values (tenant, domain, secrets) for your own environment before running. Read it first and test safely.

<#
.SYNOPSIS
    Checks every user in a Microsoft 365 / Entra tenant against Have I Been Pwned
    and produces an HTML + CSV "breach logbook".

.DESCRIPTION
    Connects to Entra ID (AzureAD module), enumerates user email addresses, queries
    the Have I Been Pwned API for each, and writes a searchable HTML report plus a CSV.

    Requires:
      - The AzureAD PowerShell module (Install-Module AzureAD)
      - A paid Have I Been Pwned API key (https://haveibeenpwned.com/API/Key)

.NOTES
    ATTRIBUTION: adapted from Kelvin Tegelaar's public breach-monitoring script at
    CyberDrain.com. Original author retains copyright; used/adapted with credit.
    Check the original's licence/terms before redistributing.

    SANITISED FOR PUBLIC RELEASE: the hardcoded HIBP API key was removed. Supply
    your own key in the config block below (better: pull it from a secret store).
#>

function Get-BreachInfo {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]$EmailAddress,
        [Parameter(Mandatory = $true)]$HaveIBeenPwnedKey,
        [Parameter(Mandatory = $true)]$Outputfile
    )
    # HTML/JS for a searchable report table
    $head = @"
<script>
function myFunction() {
    const filter = document.querySelector('#myInput').value.toUpperCase();
    const trs = document.querySelectorAll('table tr:not(.header)');
    trs.forEach(tr => tr.style.display = [...tr.children].find(td => td.innerHTML.toUpperCase().includes(filter)) ? '' : 'none');
  }</script>
<Title>Dark Web / Breach Report</Title>
<style>
body { background-color:#E5E4E2; font-family:Monospace; font-size:10pt; }
td, th { border:0px solid black; border-collapse:collapse; white-space:pre; }
th { color:white; background-color:black; }
table, tr, td, th { padding: 2px; margin: 0px; white-space:pre; }
tr:nth-child(odd) {background-color: lightgray}
table { width:95%;margin-left:5px; margin-bottom:20px; }
h2 { font-family:Tahoma; color:#6D7B8D; }
.footer { color:green; margin-left:10px; font-family:Tahoma; font-size:8pt; font-style:italic; }
#myInput {
  background-position: 10px 12px; background-repeat: no-repeat; width: 50%;
  font-size: 16px; padding: 12px 20px 12px 40px; border: 1px solid #ddd; margin-bottom: 12px;
}
</style>
"@

    $PreContent = @"
<H1> Breach logbook</H1> <br>
This log contains all breaches found for the e-mail addresses in your Microsoft tenant. Use the search to find specific addresses.
<br/><br/>
<input type="text" id="myInput" onkeyup="myFunction()" placeholder="Search...">
"@

    # HIBP requires the key in the 'hibp-api-key' header
    $HIBPHeader = @{ 'hibp-api-key' = $HaveIBeenPwnedKey }
    Write-Host "  Retrieving Breach Info" -ForegroundColor Green
    $UserList = $EmailAddress
    $index = 0
    $HIBPList = foreach ($User in $UserList) {
        $index++
        Write-Progress -Activity "Search in Progress" -Status "$([math]::Round($index/$UserList.count*100))% Complete:" -PercentComplete ($index/$UserList.count*100)
        try {
            $Breaches = $null
            $Breaches = Invoke-RestMethod -Uri "https://haveibeenpwned.com/api/v3/breachedaccount/$($user)?truncateResponse=false" -Headers $HIBPHeader -UserAgent 'PowerShell Breach Script'
        } catch {
            if ($_.Exception.Response.StatusCode.value__ -eq '404') { } else { Write-Error "$($_.Exception.message)" }
        }
        Start-Sleep 7   # HIBP rate limit - do not lower without a higher-tier key
        foreach ($Breach in $Breaches) {
            [PSCustomObject]@{
                Username              = $user
                'Name'                = $Breach.name
                'Domain name'         = $breach.Domain
                'Date'                = $Breach.Breachdate
                'Verified by experts' = if ($Breach.isverified) { 'Yes' } else { 'No' }
                'Leaked data'         = $Breach.DataClasses -join ', '
                'Description'         = $Breach.Description
            }
        }
    }
    $BreachListHTML = $HIBPList | ConvertTo-Html -Fragment -PreContent '<h2>Breaches</h2><br>A "breach" is an incident where data is inadvertently exposed. HIBP aggregates breaches so people can assess where their data has been exposed.<br>' | Out-String
    $HIBPList | Export-Csv -Path "$OutputFile.csv" -NoTypeInformation
    $head, $PreContent, [System.Web.HttpUtility]::HtmlDecode($BreachListHTML) | Out-File $Outputfile
    Write-Progress -Activity "Search in Progress" -Completed
}

# ---------------------------------------------------------------------------
# Configure for your environment
# ---------------------------------------------------------------------------
$HibpApiKey = '<HAVEIBEENPWNED_API_KEY>'   # your paid HIBP API key - do NOT hardcode a real one in a shared script

# Ensure the AzureAD module is present
$Modules = Get-Module -Name AzureAD -ListAvailable
if ($Modules.count -eq 0) {
    Write-Host "Please install the AzureAD module (as admin): Install-Module -Name AzureAD" -ForegroundColor Yellow
    Exit
}

Add-Type -AssemblyName System.Web
Connect-AzureAD

# All real user UPNs (skip the default onmicrosoft.com addresses)
$userlist = (Get-AzureADUser -All $true).UserPrincipalName | Where-Object { $_ -NotLike '*.onmicrosoft.com' }
$userlist.count
$OutputFile = "$($env:USERPROFILE)\Downloads\Breach Report $((Get-AzureADTenantDetail).DisplayName) $(Get-Date -Format 'yyyy-MM-dd').html"

Get-BreachInfo -EmailAddress $userlist -HaveIBeenPwnedKey $HibpApiKey -Outputfile $OutputFile
Invoke-Item $OutputFile