Email security & monitoring
Dark Web Breach Check: Partner Center edition
As above, with Microsoft Partner Center support so you can pick and scan any client tenant you manage. Needs a paid HIBP API key.
Adapted from: Adapted from CyberDrain (Kelvin Tegelaar) ↗
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 tenant (direct, or via Partner Center for any
managed client) against Have I Been Pwned, producing a searchable HTML + CSV report.
.DESCRIPTION
Two login options: Partner Center (pick any client tenant you manage) or a direct
Microsoft Graph login to a single tenant. Queries HIBP per user; optional Shodan
lookup if $ShodanAPIKey is set. Requires a paid HIBP API key and the PartnerCenter
and/or Microsoft.Graph modules.
.NOTES
ATTRIBUTION: adapted from Kelvin Tegelaar's public breach-monitoring script at
CyberDrain.com. Original author retains copyright; used/adapted with credit.
SANITISED FOR PUBLIC RELEASE: hardcoded HIBP API key replaced with a placeholder.
Supply your own key (better: from a secret store) - do not hardcode it in a shared script.
#>
Add-Type -AssemblyName System.Web
function Get-BreachInfo {
[CmdletBinding()]
Param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]$EmailAddress,
[Parameter(Mandatory = $true)]$HaveIBeenPwnedKey,
[Parameter(Mandatory = $true)]$Outputfile
)
$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-image: url('https://www.w3schools.com/css/searchicon.png'); /* Add a search icon to input */
background-position: 10px 12px; /* Position the search icon */
background-repeat: no-repeat; /* Do not repeat the icon image */
width: 50%; /* Full-width */
font-size: 16px; /* Increase font-size */
padding: 12px 20px 12px 40px; /* Add some padding */
border: 1px solid #ddd; /* Add a grey border */
margin-bottom: 12px; /* Add some space below the input */
}
</style>
"@
$PreContent = @"
<H1> Breach logbook</H1> <br>
This log contains all breaches found for the e-mail addresses in your Microsoft tenant. You can use the search to find specific e-mail addresses.
<br/>
<br/>
<input type="text" id="myInput" onkeyup="myFunction()" placeholder="Search...">
"@
$HIBPHeader = @{'hibp-api-key' = $HaveIBeenPwnedKey }
$HIBP_Subscription = Invoke-RestMethod -Uri 'https://haveibeenpwned.com/api/v3/subscription/status' -Headers $HIBPHeader -UserAgent 'CyberDrain.com PowerShell Breach Script'
$Delay = 60 / $HIBP_Subscription.rpm + 0.5
$est_tot_time = New-TimeSpan -Seconds ($Delay * $EmailAddress.count)
write-host " Retrieving Breach Info" -ForegroundColor Green
$UserList = $EmailAddress
$index = 0
$breachcount = 0
"HIBP Key limit $($HIBP_Subscription.rpm) Checks per minute * $($EmailAddress.count) emails = $($est_tot_time.Minutes):$($est_tot_time.Seconds) total scan time"
$HIBPList = foreach ($User in $UserList) {
$index++
Write-Progress -Activity "Search in Progress" -Status "Scanning $User - $([math]::Round($index/$UserList.count*100))% Complete:" -PercentComplete ($index/$UserList.count*100) -SecondsRemaining ($est_tot_time.TotalSeconds - ($Delay * $index));
try {
$Breaches = $null
$Breaches = Invoke-RestMethod -Uri "https://haveibeenpwned.com/api/v3/breachedaccount/$($user)?truncateResponse=false" -Headers $HIBPHeader -UserAgent 'CyberDrain.com PowerShell Breach Script'
}
catch {
if ($_.Exception.Response.StatusCode.value__ -eq '404') { } else { write-error "$($_.Exception.message)" }
}
start-sleep 7
$breachcount += $Breaches.count
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 in a vulnerable system, usually due to insufficient access controls or security weaknesses in the software. HIBP aggregates breaches and enables people to assess where their personal data has been exposed.<br>' | Out-String
$HIBPList | Export-Csv -Path "$OutputFile.csv"
if ($null -ne $ShodanAPIKey){
write-host " Getting Shodan information." -ForegroundColor Green
foreach ($Domain in $IPs) {
$ShodanQuery = (Invoke-RestMethod -Uri "https://api.shodan.io/shodan/host/search?key=$($ShodanAPIKey)&query=$Domain" -UserAgent 'CyberDrain.com PowerShell Breach Script').matches
foreach ($FoundItem in $ShodanQuery) {
[PSCustomObject]@{
'Searched for' = $Domain
'Found Product' = $FoundItem.product
'Found open port' = $FoundItem.port
'Found IP' = $FoundItem.ip_str
'Found Domain' = $FoundItem.domain
}
}
}
}
$head, $PreContent, [System.Web.HttpUtility]::HtmlDecode($BreachListHTML), $ShodanHTML | Out-File $Outputfile
Write-Progress -Activity "Search in Progress" -Completed
"$breachcount Account breaches found"
}
$loginSuccess = $false
Write-Host "Please type number or leave blank for option 1, then press [Enter]`n[1] Partner center login | use your own login`n[2] Direct Login | Use the clients credentials to login"
$option = Read-Host 'Option: 1/2/[blank]'
if ($option -eq 2){
$Module= 'Microsoft.Graph'
if((Get-Module -Name $Module -ListAvailable).count -eq 0){
Write-Host "Module $Module is missing, running a one off install now, this may take a few minutes`nInstall using below command: `nInstall-Module -Scope CurrentUser -Name $Module" -ForegroundColor yellow
Install-Module -Name $Module -AllowClobber -Scope CurrentUser
}
if (Connect-MgGraph -ContextScope Process -Scopes 'User.Read.All'){
$loginSuccess = $true
$userlist = Get-MgUser -Filter "usertype ne 'Guest'" -All -ConsistencyLevel eventual -CountVariable count
$userlist = $userlist.UserPrincipalName
$OrganiseationName = (Get-MgOrganization).DisplayName
}
}else {
<# Action when all if and elseif conditions are false #>
$Module= 'PartnerCenter'
if((Get-Module -Name $Module -ListAvailable).count -eq 0){
Write-Host "Module $Module is missing, running a one off install now, this may take a minute`nInstall using below command: `nInstall-Module -Scope CurrentUser -Name $Module" -ForegroundColor yellow
Install-Module -Name $Module -AllowClobber -Scope CurrentUser
}
if (Connect-PartnerCenter){
$loginSuccess = $true
if ($option -eq 5){#Own/self tenant
$us = Get-PartnerOrganizationProfile
$OrganiseationName = $us.CompanyName
$userlist = Get-PartnerCustomerUser -CustomerId $us.TenantId
}else{
""
"Customer selection list opened in a seperate window, it may have opened in the background"
$customer = Get-PartnerCustomer | Out-GridView -OutputMode Single
$customer
$OrganiseationName = $customer.Name
$userlist = Get-PartnerCustomerUser -CustomerId $customer.CustomerId
}
$userlist = $userlist.UserPrincipalName | Where-Object {$_ -notlike '*#EXT#@*'}
}
}
If ($loginSuccess){
#Install-Module -Name AzureAD
#Update-Module -Name AzureAD
#Connect-AzureAD
#-ConsistencyLevel eventual -CountVariable count are required for ne (Not Equal) to work https://learn.microsoft.com/en-us/graph/aad-advanced-queries?tabs=http
#$userlist = (Get-AzureADUser).UserPrincipalName | where {$_ -NotLike '*.onmicrosoft.com'}
if ($userlist.count -ne 0){
"Scanning $($userlist.count) Email accounts"
$OutputFile = "$($env:USERPROFILE)\Downloads\Breach Report $OrganiseationName $(Get-date -Format "yyyy-MM-dd").html"
Get-BreachInfo -EmailAddress $userlist -HaveIBeenPwnedKey '<HAVEIBEENPWNED_API_KEY>' -Outputfile $OutputFile
Invoke-Item $OutputFile
}
}else{
'Login failure, not scanning.'
}