Disclaimer: I do not accept responsibility for any issues arising from scripts being run without adequate understanding. It is the user's responsibility to review and assess any code before execution. More information

DNS Health Monitoring with PowerShell

When managing critical domains, monitoring DNS records for unauthorized or unexpected changes is essential. A single misconfigured A record, modified CNAME, or altered email security record can cause significant service disruption. I needed a solution that would actively monitor these changes and alert me before they became problems.

Example Alert




What the Script Monitors

The script monitors several categories of DNS records:

Infrastructure Records:

  • A records (IPv4 addresses)
  • CNAME records (canonical names)
  • DNSSEC validation (DNSKEY and DS records)

Email Security Records:

  • MX records (mail exchange)
  • SPF records (Sender Policy Framework)
  • DMARC records (Domain-based Message Authentication)
  • DKIM records (DomainKeys Identified Mail)

How It Works

The script operates on a baseline comparison model. On the first run, it queries all specified DNS records and stores them in a JSON baseline file. Subsequent runs compare live DNS values against this baseline and alert when discrepancies are detected.

Initial Baseline Creation

Running the script for the first time creates the baseline:

.\Check-DNSHealth.ps1 -DomainName "a6n.co.uk"

This generates a JSON file containing all current DNS values:

{
  "Domain": "a6n.co.uk",
  "Timestamp": "2025-10-03 14:30:22",
  "DNSSEC": {
    "Enabled": true,
    "HasDNSKEY": true,
    "HasDS": true
  },
  "Subdomains": {
    "www": {
      "A": ["34.55.229.12"],
      "CNAME": []
    }
  },
  "MX": [
    {
      "Preference": 10,
      "MailExchange": "mail.a6n.co.uk"
    }
  ]
}

Monitoring for Changes

The script checks DNS records by querying authoritative nameservers and comparing results to the baseline. When a difference is detected, it generates an alert but does not update the baseline automatically.

This was a deliberate design choice. If the baseline updated automatically after detecting a change, I would only receive a single alert. If that email was missed or filtered, an unauthorized DNS change could go unnoticed indefinitely.

The Approval Mechanism

When changes are detected, the script sends an email alert but leaves the baseline unchanged. The alert will continue on every subsequent run until the changes are explicitly approved.

To approve changes and update the baseline:

.\Check-DNSHealth.ps1 -DomainName "a6n.co.uk" -UpdateBaseline

This command tells the script: "I have reviewed these DNS changes and they are authorized. Update the baseline to reflect the new values."

Email Rate Limiting

To prevent inbox flooding during ongoing DNS issues, the script includes built-in rate limiting. By default, it sends a maximum of 4 alert emails per hour. This ensures persistence without spam.

# Run with custom rate limit (10 emails per hour)
.\Check-DNSHealth.ps1 -DomainName "example.com" `
    -SMTPServer "smtprelay.internal.local" `
    -EmailFrom "dns-alerts@example.com" `
    -EmailTo "admin@example.com" `
    -MaxEmailsPerHour 10

The rate limit counter resets on a rolling 60-minute window. If the limit is reached, the script displays:

Email rate limit reached (4/4 in last hour)
Next email available in approximately 23 minute(s)

Configuring Monitored Records

Subdomain Monitoring

By default, the script monitors these subdomains: adfsautodiscoverremotewwwwww2. To monitor custom subdomains:

.\Check-DNSHealth.ps1 -DomainName "a6n.co.uk" `
    -Subdomains @("mail","vpn","portal","api")

DKIM Selector Configuration

DKIM records require explicit selector specification. The script defaults to selector1 and selector2, but custom selectors are supported:

.\Check-DNSHealth.ps1 -DomainName "example.com" `
    -DKIMSelectors @("selector1","selector2","google")

SMTP Configuration

The script supports various SMTP configurations. For internal relay servers that don't require authentication:

.\Check-DNSHealth.ps1 -DomainName "a6n.co.uk" `
    -SMTPServer "smtprelay.bear.local" `
    -EmailFrom "dns-monitor@bear.local" `
    -EmailTo "lee@bear.local"

For external SMTP servers requiring SSL/TLS (like Office 365):

.\Check-DNSHealth.ps1 -DomainName "example.com" `
    -SMTPServer "smtp.office365.com" `
    -SMTPPort 587 `
    -UseSSL `
    -EmailFrom "dns-alerts@bythepowerofgreyskull.com" `
    -EmailTo "lee@bythepowerofgreyskull.com"

Record Comparison Logic

The script implements intelligent comparison logic to avoid false positives:

  • MX Records: Sorted before comparison to prevent alerts when multiple MX records are returned in different orders by DNS servers.
  • A/CNAME Records: Sorted alphabetically to handle round-robin DNS configurations.
  • TXT Records: Compared as concatenated strings to handle multi-string TXT records correctly.

Scheduled Monitoring

For continuous monitoring, I schedule the script to run every 15 minutes:

$action = New-ScheduledTaskAction -Execute "powershell.exe" `
    -Argument "-File C:\Scripts\Check-DNSHealth.ps1 -DomainName 'a6n.co.uk' -SMTPServer 'smtprelay.bear.local' -EmailFrom 'dns-report@bear.local' -EmailTo 'lee@bear.local'"

$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 15) -RepetitionDuration ([TimeSpan]::MaxValue)

Register-ScheduledTask -TaskName "DNS Monitor - example.com" -Action $action -Trigger $trigger -User "SYSTEM"

DNSSEC Validation

The script validates DNSSEC by checking for both DNSKEY and DS records:

# Checks for DNSKEY at the domain
$dnskey = Resolve-DnsName -Name $Domain -Type DNSKEY -DnssecOk

# Checks for DS records at parent zone
$ds = Resolve-DnsName -Name $Domain -Type DS -Server "8.8.8.8"

DNSSEC is considered enabled if either DNSKEY or DS records are present. The script reports:

DNSSEC Enabled: True
  HasDNSKEY: True
  HasDS: True
  DNSKEYCount: 2

Script Workflow

  1. Initial setup: Run script to create baseline
  2. Schedule monitoring: Configure scheduled task to run every 15 minutes
  3. Receive alert: DNS change detected, email sent
  4. Investigate: Review the changes in the email
  5. Approve or remediate:
    • If legitimate: Run with -UpdateBaseline to approve
    • If unauthorized: Remediate DNS, alert will stop automatically when records match baseline again

The approval mechanism ensures that I cannot ignore DNS changes. The alerts continue until I explicitly acknowledge them, providing a forcing function for security review.

Command Syntax Reference

Basic Syntax

.\Check-DNSHealth.ps1 -DomainName <string> 
    [-Subdomains <string[]>] 
    [-DKIMSelectors <string[]>] 
    [-SMTPServer <string>] 
    [-SMTPPort <int>] 
    [-UseSSL] 
    [-EmailFrom <string>] 
    [-EmailTo <string[]>] 
    [-UpdateBaseline] 
    [-MaxEmailsPerHour <int>]

Script : Check-DNSHealth.ps1

<#
.SYNOPSIS
    DNS Health Monitoring Script with DNSSEC validation and baseline comparison
.DESCRIPTION
    Monitors DNS records including DNSSEC, A, CNAME, MX, TXT (SPF, DMARC, DKIM)
    Creates baseline on first run, compares subsequent runs, and generates HTML email alerts
    Requires -UpdateBaseline switch to approve changes and update baseline
    Includes email rate limiting to prevent spam
.PARAMETER DomainName
    The domain name to check (e.g., example.com)
.PARAMETER Subdomains
    Array of subdomains to check (default: adfs, autodiscover, remote, www, www2)
.PARAMETER DKIMSelectors
    Array of DKIM selectors to check (default: mailjet, pps1)
.PARAMETER SMTPServer
    SMTP server for sending email alerts
.PARAMETER SMTPPort
    SMTP port (default: 25)
.PARAMETER UseSSL
    Use SSL for SMTP connection
.PARAMETER EmailFrom
    Sender email address
.PARAMETER EmailTo
    Recipient email address(es)
.PARAMETER UpdateBaseline
    Approve current DNS values and update baseline (no email sent)
.PARAMETER MaxEmailsPerHour
    Maximum number of alert emails to send per hour (default: 4)
.EXAMPLE
    .\Check-DNSHealth.ps1 -DomainName "contoso.com"
.EXAMPLE
    .\Check-DNSHealth.ps1 -DomainName "contoso.com" -SMTPServer "smtprelay.internal.local" -EmailFrom "alerts@contoso.com" -EmailTo "admin@contoso.com"
.EXAMPLE
    .\Check-DNSHealth.ps1 -DomainName "contoso.com" -UpdateBaseline
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory=$true)]
    [string]$DomainName,
    
    [Parameter(Mandatory=$false)]
    [string[]]$Subdomains = @("adfs", "autodiscover", "remote", "www", "www2"),
    
    [Parameter(Mandatory=$false)]
    [string[]]$DKIMSelectors = @("mailjet", "pps1"),
    
    [Parameter(Mandatory=$false)]
    [string]$SMTPServer,
    
    [Parameter(Mandatory=$false)]
    [int]$SMTPPort = 25,
    
    [Parameter(Mandatory=$false)]
    [switch]$UseSSL,
    
    [Parameter(Mandatory=$false)]
    [string]$EmailFrom,
    
    [Parameter(Mandatory=$false)]
    [string[]]$EmailTo,
    
    [Parameter(Mandatory=$false)]
    [switch]$UpdateBaseline,
    
    [Parameter(Mandatory=$false)]
    [int]$MaxEmailsPerHour = 4
)

# File paths
$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
$baselineFile = Join-Path $scriptPath "$($DomainName -replace '\.','_')_baseline.json"
$emailLogFile = Join-Path $scriptPath "$($DomainName -replace '\.','_')_email_log.json"

# Initialize results object
$results = @{
    Domain = $DomainName
    Timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
    DNSSEC = @{}
    Subdomains = @{}
    MX = @()
    SPF = @()
    DMARC = @()
    DKIM = @{}
}

#region Functions

# Email rate limiting function
function Test-EmailRateLimit {
    param(
        [string]$LogFile,
        [int]$MaxPerHour
    )
    
    $now = Get-Date
    $oneHourAgo = $now.AddHours(-1)
    
    if(Test-Path $LogFile) {
        try {
            $emailLog = Get-Content $LogFile | ConvertFrom-Json
            $recentEmails = $emailLog | Where-Object { 
                [DateTime]$_.Timestamp -gt $oneHourAgo 
            }
            
            $emailCount = @($recentEmails).Count
            
            if($emailCount -ge $MaxPerHour) {
                $oldestRecentEmail = ($recentEmails | Sort-Object Timestamp | Select-Object -First 1).Timestamp
                $waitMinutes = [math]::Ceiling((([DateTime]$oldestRecentEmail).AddHours(1) - $now).TotalMinutes)
                
                Write-Host "  Email rate limit reached ($emailCount/$MaxPerHour in last hour)" -ForegroundColor Yellow
                Write-Host "  Next email available in approximately $waitMinutes minute(s)" -ForegroundColor Yellow
                return $false
            }
            
            return $true
        }
        catch {
            Write-Host "  Warning: Could not read email log, allowing email" -ForegroundColor Yellow
            return $true
        }
    }
    
    return $true
}

# Log email sent
function Add-EmailLog {
    param([string]$LogFile, [string]$Domain)
    
    $emailLog = @()
    
    if(Test-Path $LogFile) {
        try {
            $existingLog = Get-Content $LogFile -Raw | ConvertFrom-Json
            # Convert to array if it's not already
            if($existingLog -is [array]) {
                $emailLog = @($existingLog)
            } else {
                $emailLog = @($existingLog)
            }
        }
        catch {
            $emailLog = @()
        }
    }
    
    # Add new entry
    $newEntry = @{
        Timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
        Domain = $Domain
    }
    
    $emailLog = $emailLog + $newEntry
    
    # Keep only last 24 hours of logs
    $oneDayAgo = (Get-Date).AddDays(-1)
    $emailLog = @($emailLog | Where-Object { 
        [DateTime]$_.Timestamp -gt $oneDayAgo 
    })
    
    # Save back to file
    $emailLog | ConvertTo-Json -Depth 10 | Out-File $LogFile -Encoding UTF8
}

# Function to check DNSSEC
function Test-DNSSEC {
    param([string]$Domain)
    
    Write-Host "Checking DNSSEC..." -ForegroundColor Yellow
    
    try {
        $dnskey = Resolve-DnsName -Name $Domain -Type DNSKEY -DnssecOk -ErrorAction SilentlyContinue
        $ds = Resolve-DnsName -Name $Domain -Type DS -Server "8.8.8.8" -ErrorAction SilentlyContinue
        
        $dnssecEnabled = ($null -ne $dnskey) -or ($null -ne $ds)
        
        $dnssecInfo = @{
            Enabled = $dnssecEnabled
            HasDNSKEY = ($null -ne $dnskey)
            HasDS = ($null -ne $ds)
            DNSKEYCount = if($dnskey){@($dnskey).Count}else{0}
        }
        
        Write-Host "  DNSSEC Enabled: $dnssecEnabled" -ForegroundColor $(if($dnssecEnabled){"Green"}else{"Red"})
        
        return $dnssecInfo
    }
    catch {
        Write-Host "  Error checking DNSSEC: $($_.Exception.Message)" -ForegroundColor Red
        return @{Enabled = $false; Error = $_.Exception.Message}
    }
}

# Function to check subdomain records
function Get-SubdomainRecords {
    param([string]$Subdomain, [string]$Domain)
    
    $fqdn = if($Subdomain -eq "@") { $Domain } else { "$Subdomain.$Domain" }
    
    try {
        $records = @{
            FQDN = $fqdn
            A = @()
            CNAME = @()
        }
        
        $aRecords = Resolve-DnsName -Name $fqdn -Type A -ErrorAction SilentlyContinue
        if($aRecords) {
            $records.A = $aRecords | Where-Object {$_.Type -eq "A"} | ForEach-Object { $_.IPAddress }
        }
        
        $cnameRecords = Resolve-DnsName -Name $fqdn -Type CNAME -ErrorAction SilentlyContinue
        if($cnameRecords) {
            $records.CNAME = $cnameRecords | Where-Object {$_.Type -eq "CNAME"} | ForEach-Object { $_.NameHost }
        }
        
        return $records
    }
    catch {
        return @{FQDN = $fqdn; Error = $_.Exception.Message}
    }
}

# Function to get MX records
function Get-MXRecords {
    param([string]$Domain)
    
    Write-Host "Checking MX records..." -ForegroundColor Yellow
    
    try {
        $mx = Resolve-DnsName -Name $Domain -Type MX -ErrorAction SilentlyContinue
        $mxRecords = $mx | Where-Object {$_.Type -eq "MX"} | ForEach-Object {
            @{
                Preference = $_.Preference
                MailExchange = $_.NameExchange
            }
        } | Sort-Object Preference
        
        Write-Host "  Found $(@($mxRecords).Count) MX record(s)" -ForegroundColor Green
        return $mxRecords
    }
    catch {
        Write-Host "  Error checking MX: $($_.Exception.Message)" -ForegroundColor Red
        return @()
    }
}

# Function to get SPF records
function Get-SPFRecords {
    param([string]$Domain)
    
    Write-Host "Checking SPF records..." -ForegroundColor Yellow
    
    try {
        $txt = Resolve-DnsName -Name $Domain -Type TXT -ErrorAction SilentlyContinue
        $spfRecords = $txt | Where-Object {$_.Strings -like "v=spf1*"} | ForEach-Object { $_.Strings -join "" }
        
        Write-Host "  Found $(@($spfRecords).Count) SPF record(s)" -ForegroundColor Green
        return $spfRecords
    }
    catch {
        Write-Host "  Error checking SPF: $($_.Exception.Message)" -ForegroundColor Red
        return @()
    }
}

# Function to get DMARC records
function Get-DMARCRecords {
    param([string]$Domain)
    
    Write-Host "Checking DMARC records..." -ForegroundColor Yellow
    
    try {
        $dmarcDomain = "_dmarc.$Domain"
        $txt = Resolve-DnsName -Name $dmarcDomain -Type TXT -ErrorAction SilentlyContinue
        $dmarcRecords = $txt | Where-Object {$_.Strings -like "v=DMARC1*"} | ForEach-Object { $_.Strings -join "" }
        
        Write-Host "  Found $(@($dmarcRecords).Count) DMARC record(s)" -ForegroundColor Green
        return $dmarcRecords
    }
    catch {
        Write-Host "  Error checking DMARC: $($_.Exception.Message)" -ForegroundColor Red
        return @()
    }
}

# Function to get DKIM records
function Get-DKIMRecords {
    param([string]$Selector, [string]$Domain)
    
    try {
        $dkimDomain = "$Selector._domainkey.$Domain"
        $txt = Resolve-DnsName -Name $dkimDomain -Type TXT -ErrorAction SilentlyContinue
        $dkimRecord = $txt | Where-Object {$_.Strings -like "*v=DKIM1*"} | ForEach-Object { $_.Strings -join "" }
        
        return @{
            Selector = $Selector
            Record = if($dkimRecord){$dkimRecord}else{$null}
            Found = ($null -ne $dkimRecord)
        }
    }
    catch {
        return @{
            Selector = $Selector
            Record = $null
            Found = $false
            Error = $_.Exception.Message
        }
    }
}

#endregion

# Main Script Start
Write-Host "=== DNS Health Check for $DomainName ===" -ForegroundColor Cyan
Write-Host "Timestamp: $($results.Timestamp)" -ForegroundColor Gray

if($UpdateBaseline) {
    Write-Host "Mode: APPROVAL - Will update baseline after checks" -ForegroundColor Magenta
} else {
    Write-Host "Mode: MONITORING - Baseline will NOT be updated (use -UpdateBaseline to approve changes)" -ForegroundColor Yellow
}

Write-Host ""

# Run all DNS checks
$results.DNSSEC = Test-DNSSEC -Domain $DomainName

Write-Host ""
Write-Host "Checking subdomain records..." -ForegroundColor Yellow
foreach($sub in $Subdomains) {
    $subRecords = Get-SubdomainRecords -Subdomain $sub -Domain $DomainName
    $results.Subdomains[$sub] = $subRecords
    Write-Host "  $sub : A=$(@($subRecords.A).Count) CNAME=$(@($subRecords.CNAME).Count)" -ForegroundColor Green
}

Write-Host ""
$results.MX = Get-MXRecords -Domain $DomainName

Write-Host ""
$results.SPF = Get-SPFRecords -Domain $DomainName

Write-Host ""
$results.DMARC = Get-DMARCRecords -Domain $DomainName

Write-Host ""
Write-Host "Checking DKIM records..." -ForegroundColor Yellow
foreach($selector in $DKIMSelectors) {
    $dkimResult = Get-DKIMRecords -Selector $selector -Domain $DomainName
    $results.DKIM[$selector] = $dkimResult
    $status = if($dkimResult.Found){"Found"}else{"Not Found"}
    $color = if($dkimResult.Found){"Green"}else{"Yellow"}
    Write-Host "  $selector : $status" -ForegroundColor $color
}

# Check if baseline exists and compare
if(Test-Path $baselineFile) {
    Write-Host ""
    Write-Host "=== Comparing with Baseline ===" -ForegroundColor Cyan
    
    $baseline = Get-Content $baselineFile | ConvertFrom-Json
    $differences = @()
    
    # Compare DNSSEC
    if($results.DNSSEC.Enabled -ne $baseline.DNSSEC.Enabled) {
        $differences += @{
            Category = "DNSSEC"
            Field = "Enabled"
            Baseline = $baseline.DNSSEC.Enabled
            Current = $results.DNSSEC.Enabled
        }
    }
    
    # Compare Subdomains
    foreach($sub in $Subdomains) {
        $currentA = $results.Subdomains.$sub.A | Sort-Object
        $baselineA = $baseline.Subdomains.$sub.A | Sort-Object
        
        if(($currentA -join ",") -ne ($baselineA -join ",")) {
            $differences += @{
                Category = "Subdomain"
                Field = "$sub (A Records)"
                Baseline = ($baselineA -join ", ")
                Current = ($currentA -join ", ")
            }
        }
        
        $currentCNAME = $results.Subdomains.$sub.CNAME | Sort-Object
        $baselineCNAME = $baseline.Subdomains.$sub.CNAME | Sort-Object
        
        if(($currentCNAME -join ",") -ne ($baselineCNAME -join ",")) {
            $differences += @{
                Category = "Subdomain"
                Field = "$sub (CNAME Records)"
                Baseline = ($baselineCNAME -join ", ")
                Current = ($currentCNAME -join ", ")
            }
        }
    }
    
    # Compare MX (sort to avoid false positives)
    $currentMX = ($results.MX | ForEach-Object {"$($_.Preference):$($_.MailExchange)"} | Sort-Object) -join ","
    $baselineMX = ($baseline.MX | ForEach-Object {"$($_.Preference):$($_.MailExchange)"} | Sort-Object) -join ","
    
    if($currentMX -ne $baselineMX) {
        $differences += @{
            Category = "MX"
            Field = "MX Records"
            Baseline = $baselineMX
            Current = $currentMX
        }
    }
    
    # Compare SPF
    if(($results.SPF -join "") -ne ($baseline.SPF -join "")) {
        $differences += @{
            Category = "SPF"
            Field = "SPF Record"
            Baseline = ($baseline.SPF -join "")
            Current = ($results.SPF -join "")
        }
    }
    
    # Compare DMARC
    if(($results.DMARC -join "") -ne ($baseline.DMARC -join "")) {
        $differences += @{
            Category = "DMARC"
            Field = "DMARC Record"
            Baseline = ($baseline.DMARC -join "")
            Current = ($results.DMARC -join "")
        }
    }
    
    # Compare DKIM
    foreach($selector in $DKIMSelectors) {
        if($results.DKIM.$selector.Record -ne $baseline.DKIM.$selector.Record) {
            $differences += @{
                Category = "DKIM"
                Field = "$selector Selector"
                Baseline = $baseline.DKIM.$selector.Record
                Current = $results.DKIM.$selector.Record
            }
        }
    }
    
    # Handle differences
    if($differences.Count -gt 0) {
        Write-Host ""
        Write-Host "⚠ CHANGES DETECTED: $($differences.Count) difference(s) found!" -ForegroundColor Red
        
        foreach($diff in $differences) {
            Write-Host "  [$($diff.Category)] $($diff.Field)" -ForegroundColor Yellow
            Write-Host "    Baseline: $($diff.Baseline)" -ForegroundColor Gray
            Write-Host "    Current:  $($diff.Current)" -ForegroundColor White
        }
        
        if($UpdateBaseline) {
            Write-Host ""
            Write-Host "✓ -UpdateBaseline specified: Approving changes and updating baseline..." -ForegroundColor Green
            $results | ConvertTo-Json -Depth 10 | Out-File $baselineFile -Encoding UTF8
            Write-Host "  Baseline updated: $baselineFile" -ForegroundColor Green
            Write-Host "  These changes are now approved and will not trigger future alerts" -ForegroundColor Gray
        }
        else {
            Write-Host ""
            Write-Host "⚠ Baseline NOT updated (monitoring mode)" -ForegroundColor Yellow
            Write-Host "  These changes will continue to generate alerts until approved" -ForegroundColor Gray
            Write-Host "  To approve these changes, run:" -ForegroundColor Cyan
            Write-Host "    .\Check-DNSHealth.ps1 -DomainName `"$DomainName`" -UpdateBaseline" -ForegroundColor White
            
            # Send email if configured and not in update mode
            if($SMTPServer -and $EmailFrom -and $EmailTo) {
                Write-Host ""
                
                if(Test-EmailRateLimit -LogFile $emailLogFile -MaxPerHour $MaxEmailsPerHour) {
                    Write-Host "Generating email alert..." -ForegroundColor Yellow
                    
                    $htmlBody = @"
<!DOCTYPE html>
<html>
<head>
    <style>
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f5f5f5; margin: 0; padding: 20px; }
        .container { max-width: 800px; margin: 0 auto; background-color: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
        .header { background-color: #d32f2f; color: white; padding: 30px; border-radius: 8px 8px 0 0; }
        .header h1 { margin: 0; font-size: 24px; font-weight: 500; }
        .header p { margin: 10px 0 0 0; opacity: 0.9; font-size: 14px; }
        .content { padding: 30px; }
        .alert-box { background-color: #fff3e0; border-left: 4px solid #ff9800; padding: 15px; margin-bottom: 25px; border-radius: 4px; }
        .alert-box strong { color: #e65100; }
        .approval-box { background-color: #e3f2fd; border-left: 4px solid #2196f3; padding: 15px; margin-bottom: 25px; border-radius: 4px; font-family: 'Courier New', monospace; font-size: 13px; }
        .approval-box strong { color: #1565c0; display: block; margin-bottom: 8px; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
        table { width: 100%; border-collapse: collapse; margin-top: 20px; }
        th { background-color: #f5f5f5; padding: 12px; text-align: left; font-weight: 600; color: #333; border-bottom: 2px solid #ddd; }
        td { padding: 12px; border-bottom: 1px solid #eee; vertical-align: top; }
        .category { font-weight: 600; color: #1976d2; }
        .baseline { color: #666; font-family: 'Courier New', monospace; font-size: 13px; }
        .current { color: #d32f2f; font-family: 'Courier New', monospace; font-size: 13px; font-weight: 600; }
        .footer { padding: 20px 30px; background-color: #f5f5f5; border-radius: 0 0 8px 8px; text-align: center; font-size: 12px; color: #666; }
        .timestamp { color: #999; font-size: 11px; }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>⚠️ DNS Configuration Changes Detected</h1>
            <p>Domain: $DomainName</p>
        </div>
        <div class="content">
            <div class="alert-box">
                <strong>$($differences.Count) DNS record change(s)</strong> detected compared to baseline
            </div>
            <div class="approval-box">
                <strong>⚠ APPROVAL REQUIRED</strong>
                To approve these changes and update the baseline, run:<br>
                .\Check-DNSHealth.ps1 -DomainName "$DomainName" -UpdateBaseline
            </div>
            <table>
                <tr>
                    <th>Category</th>
                    <th>Record</th>
                    <th>Baseline Value</th>
                    <th>Current Value</th>
                </tr>
"@
                    foreach($diff in $differences) {
                        $baselineEncoded = [System.Web.HttpUtility]::HtmlEncode($diff.Baseline)
                        $currentEncoded = [System.Web.HttpUtility]::HtmlEncode($diff.Current)
                        $htmlBody += @"
                <tr>
                    <td class="category">$($diff.Category)</td>
                    <td>$($diff.Field)</td>
                    <td class="baseline">$baselineEncoded</td>
                    <td class="current">$currentEncoded</td>
                </tr>
"@
                    }
                    
                    $htmlBody += @"
            </table>
        </div>
        <div class="footer">
            <p class="timestamp">Check performed: $($results.Timestamp)</p>
            <p>Automated DNS Health Monitoring System</p>
        </div>
    </div>
</body>
</html>
"@
                    
                    try {
                        Add-Type -AssemblyName System.Web
                        
                        $mailParams = @{
                            From = $EmailFrom
                            To = $EmailTo
                            Subject = "DNS Alert: Changes Detected for $DomainName - APPROVAL REQUIRED"
                            Body = $htmlBody
                            BodyAsHtml = $true
                            SmtpServer = $SMTPServer
                            Port = $SMTPPort
                        }
                        
                        if($UseSSL) {
                            $mailParams.UseSsl = $true
                        }
                        
                        Send-MailMessage @mailParams
                        Write-Host "  ✓ Email alert sent successfully!" -ForegroundColor Green
                        
                        # Log the email
                        try {
                            Add-EmailLog -LogFile $emailLogFile -Domain $DomainName
                        }
                        catch {
                            Write-Host "  Warning: Could not log email send (non-critical): $($_.Exception.Message)" -ForegroundColor Yellow
                        }
                        
                    }
                    catch {
                        Write-Host "  ✗ Error sending email: $($_.Exception.Message)" -ForegroundColor Red
                        
                        # Save HTML to file as backup
                        $htmlFile = Join-Path $scriptPath "$($DomainName -replace '\.','_')_alert_$(Get-Date -Format 'yyyyMMdd_HHmmss').html"
                        $htmlBody | Out-File $htmlFile -Encoding UTF8
                        Write-Host "  HTML report saved to: $htmlFile" -ForegroundColor Yellow
                    }
                }
                else {
                    Write-Host "  ⊘ Email suppressed due to rate limiting" -ForegroundColor Yellow
                }
            }
        }
    }
    else {
        Write-Host ""
        Write-Host "✓ No changes detected - all DNS records match baseline" -ForegroundColor Green
        
        if($UpdateBaseline) {
            Write-Host "  No updates needed (already in sync)" -ForegroundColor Gray
        }
    }
}
else {
    # No baseline exists - create initial baseline
    Write-Host ""
    Write-Host "=== Creating Initial Baseline ===" -ForegroundColor Cyan
    Write-Host "No baseline found. Creating new baseline..." -ForegroundColor Yellow
    
    $results | ConvertTo-Json -Depth 10 | Out-File $baselineFile -Encoding UTF8
    
    Write-Host "  ✓ Baseline created: $baselineFile" -ForegroundColor Green
    Write-Host "  Future runs will compare against this baseline" -ForegroundColor Gray
}

Write-Host ""
Write-Host "=== DNS Health Check Complete ===" -ForegroundColor Cyan
Write-Host ""

Output Files

The script creates files in the same directory where it runs:

{domain}_baseline.json

  • Contains the approved DNS configuration
  • Only updated when -UpdateBaseline is specified
  • Example: contoso_com_baseline.json

{domain}_email_log.json

  • Tracks email alerts for rate limiting
  • Automatically prunes entries older than 24 hours
  • Example: contoso_com_email_log.json

{domain}_alert_{timestamp}.html (backup only)

  • Created only when email delivery fails
  • Contains the alert email as HTML
  • Example: contoso_com_alert_20251003_204506.html

The approval mechanism ensures that I cannot ignore DNS changes. The alerts continue until I explicitly acknowledge them, providing a forcing function for security review.

Previous Post Next Post

نموذج الاتصال